diff --git a/pom.xml b/pom.xml index cc5049eb..f344cc33 100644 --- a/pom.xml +++ b/pom.xml @@ -97,7 +97,7 @@ io.quarkus quarkus-smallrye-fault-tolerance - + org.hibernate hibernate-jpamodelgen @@ -106,7 +106,7 @@ io.quarkus quarkus-spring-data-jpa - + com.querydsl querydsl-jpa @@ -198,6 +198,45 @@ rest-assured test + + + io.github.m-m-m + mmm-base + 0.2.0 + + + + net.sf.m-m-m + mmm-util-validation + 8.7.0 + + + + org.assertj + assertj-core + test + + + + org.hibernate.validator + hibernate-validator + test + + + + javax.el + javax.el-api + 2.2.4 + test + + + + org.glassfish + javax.el + 3.0.1-b11 + test + + @@ -238,8 +277,8 @@ - - + + com.mysema.maven apt-maven-plugin diff --git a/src/main/java/com/devonfw/quarkus/exceptionutils/ProductNotFoundException.java b/src/main/java/com/devonfw/quarkus/exceptionutils/ProductNotFoundException.java new file mode 100644 index 00000000..ed9f7341 --- /dev/null +++ b/src/main/java/com/devonfw/quarkus/exceptionutils/ProductNotFoundException.java @@ -0,0 +1,17 @@ +package com.devonfw.quarkus.exceptionutils; + +import net.sf.mmm.util.exception.api.NlsRuntimeException; + +public class ProductNotFoundException extends NlsRuntimeException { + + private final String message; + + /** + * The constructor. + */ + public ProductNotFoundException(String message) { + + this.message = message; + } + +} diff --git a/src/main/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacade.java b/src/main/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacade.java new file mode 100644 index 00000000..39f45a11 --- /dev/null +++ b/src/main/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacade.java @@ -0,0 +1,460 @@ +package com.devonfw.quarkus.exceptionutils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Path.Node; +import javax.validation.ValidationException; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import net.sf.mmm.util.exception.api.NlsRuntimeException; +import net.sf.mmm.util.exception.api.NlsThrowable; +import net.sf.mmm.util.exception.api.TechnicalErrorUserException; +import net.sf.mmm.util.exception.api.ValidationErrorUserException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * This is an implementation of {@link ExceptionMapper} that acts as generic exception facade for REST services. It + * {@link #toResponse(Throwable) maps} {@link Throwable exceptions} to an according HTTP status code and JSON result as + * defined by + * devonfw + * REST error specification. + * + * @since 2.0.0 + */ +@Provider +@Priority(value = 1) +public class RestServiceExceptionFacade implements ExceptionMapper { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(RestServiceExceptionFacade.class); + + private final Class rollbackException; + + private ObjectMapper mapper; + + private boolean exposeInternalErrorDetails; + + /** + * The constructor. + */ + public RestServiceExceptionFacade() { + + super(); + this.rollbackException = loadException("javax.persistence.RollbackException"); + } + + private Class loadException(String className) { + + try { + @SuppressWarnings("unchecked") + Class exception = (Class) Class.forName(className); + return exception; + } catch (ClassNotFoundException e) { + LOG.info("Exception {} was not found on classpath and can not be handled by this {}.", className, + getClass().getSimpleName()); + } catch (Exception e) { + LOG.error("Exception {} is invalid and can not be handled by this {}.", className, getClass().getSimpleName(), e); + } + return null; + } + + @Override + public Response toResponse(Throwable exception) { + + if (exception instanceof WebApplicationException) { + return createResponse((WebApplicationException) exception); + } else if (exception instanceof NlsRuntimeException) { + return toResponse(exception, exception); + } else { + Throwable error = exception; + Throwable catched = exception; + error = getRollbackCause(exception); + if (error == null) { + error = unwrapNlsUserError(exception); + } + if (error == null) { + error = exception; + } + return toResponse(error, catched); + } + } + + /** + * Unwraps potential NLS user error from a wrapper exception such as {@code JsonMappingException} or + * {@code PersistenceException}. + * + * @param exception the exception to unwrap. + * @return the unwrapped {@link NlsRuntimeException} exception or {@code null} if no + * {@link NlsRuntimeException#isForUser() use error}. + */ + private NlsRuntimeException unwrapNlsUserError(Throwable exception) { + + Throwable cause = exception.getCause(); + if (cause instanceof NlsRuntimeException) { + NlsRuntimeException nlsError = (NlsRuntimeException) cause; + if (nlsError.isForUser()) { + return nlsError; + } + } + return null; + } + + private Throwable getRollbackCause(Throwable exception) { + + Class exceptionClass = exception.getClass(); + // if (exceptionClass == this.transactionSystemException) { + Throwable cause = exception.getCause(); + if (cause != null) { + exceptionClass = cause.getClass(); + if (exceptionClass == this.rollbackException) { + return cause.getCause(); + } + } + // } + return null; + } + + /** + * @see #toResponse(Throwable) + * + * @param exception the exception to handle + * @param catched the original exception that was cached. Either same as {@code error} or a (child-) + * {@link Throwable#getCause() cause} of it. + * @return the response build from the exception. + */ + protected Response toResponse(Throwable exception, Throwable catched) { + + if (exception instanceof ValidationException) { + return handleValidationException(exception, catched); + } else if (exception instanceof ValidationErrorUserException) { + return createResponse(exception, (ValidationErrorUserException) exception, null); + } else { + return handleGenericError(exception, catched); + } + } + + /** + * Creates the {@link Response} for the given validation exception. + * + * @param exception is the original validation exception. + * @param error is the wrapped exception or the same as exception. + * @param errorsMap is a map with all validation errors + * @return the requested {@link Response}. + */ + protected Response createResponse(Throwable exception, ValidationErrorUserException error, + Map> errorsMap) { + + LOG.warn("Service failed due to validation failure.", error); + if (exception == error) { + return createResponse(Status.BAD_REQUEST, error, errorsMap); + } else { + return createResponse(Status.BAD_REQUEST, error, exception.getMessage(), errorsMap); + } + } + + /** + * Exception handling for generic exception (fallback). + * + * @param exception the exception to handle + * @param catched the original exception that was cached. Either same as {@code error} or a (child-) + * {@link Throwable#getCause() cause} of it.z + * @return the response build from the exception + */ + protected Response handleGenericError(Throwable exception, Throwable catched) { + + NlsRuntimeException userError; + boolean logged = false; + if (exception instanceof NlsThrowable) { + NlsThrowable nlsError = (NlsThrowable) exception; + if (!nlsError.isTechnical()) { + LOG.warn("Service failed due to business error: {}", nlsError.getMessage()); + logged = true; + } + userError = TechnicalErrorUserException.getOrCreateUserException(exception); + } else { + userError = TechnicalErrorUserException.getOrCreateUserException(catched); + } + if (!logged) { + LOG.error("Service failed on server", userError); + } + return createResponse(userError); + } + + /** + * Exception handling for validation exception. + * + * @param exception the exception to handle + * @param catched the original exception that was cached. Either same as {@code error} or a (child-) + * {@link Throwable#getCause() cause} of it. + * @return the response build from the exception. + */ + protected Response handleValidationException(Throwable exception, Throwable catched) { + + Throwable t = catched; + Map> errorsMap = null; + if (exception instanceof ConstraintViolationException) { + ConstraintViolationException constraintViolationException = (ConstraintViolationException) exception; + Set> violations = constraintViolationException.getConstraintViolations(); + errorsMap = new HashMap<>(); + + for (ConstraintViolation violation : violations) { + Iterator it = violation.getPropertyPath().iterator(); + String fieldName = null; + + // Getting fieldname from the exception + while (it.hasNext()) { + fieldName = it.next().toString(); + } + + List errorsList = errorsMap.get(fieldName); + + if (errorsList == null) { + errorsList = new ArrayList<>(); + errorsMap.put(fieldName, errorsList); + } + + errorsList.add(violation.getMessage()); + + } + + t = new ValidationException(errorsMap.toString(), catched); + } + ValidationErrorUserException error = new ValidationErrorUserException(t); + return createResponse(t, error, errorsMap); + } + + /** + * @param error is the {@link Throwable} to extract message details from. + * @return the exposed message(s). + */ + protected String getExposedErrorDetails(Throwable error) { + + StringBuilder buffer = new StringBuilder(); + Throwable e = error; + while (e != null) { + if (buffer.length() > 0) { + buffer.append(System.lineSeparator()); + } + buffer.append(e.getClass().getSimpleName()); + buffer.append(": "); + buffer.append(e.getMessage()); + e = e.getCause(); + } + return buffer.toString(); + } + + /** + * Create the {@link Response} for the given {@link NlsRuntimeException}. + * + * @param error the generic {@link NlsRuntimeException}. + * @return the corresponding {@link Response}. + */ + protected Response createResponse(NlsRuntimeException error) { + + Status status; + if (error.isTechnical()) { + status = Status.INTERNAL_SERVER_ERROR; + } else { + status = Status.BAD_REQUEST; + } + return createResponse(status, error, null); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param status is the HTTP {@link Status}. + * @param error is the catched or wrapped {@link NlsRuntimeException}. + * @param errorsMap is a map with all validation errors + * @return the corresponding {@link Response}. + */ + protected Response createResponse(Status status, NlsRuntimeException error, Map> errorsMap) { + + String message; + if (this.exposeInternalErrorDetails) { + message = getExposedErrorDetails(error); + } else { + message = error.getMessage(); + } + return createResponse(status, error, message, errorsMap); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param status is the HTTP {@link Status}. + * @param error is the catched or wrapped {@link NlsRuntimeException}. + * @param message is the JSON message attribute. + * @param errorsMap is a map with all validation errors + * @return the corresponding {@link Response}. + */ + protected Response createResponse(Status status, NlsRuntimeException error, String message, + Map> errorsMap) { + + return createResponse(status, error, message, error.getCode(), errorsMap); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param status is the HTTP {@link Status}. + * @param error is the catched or wrapped {@link NlsRuntimeException}. + * @param message is the JSON message attribute. + * @param code is the {@link NlsRuntimeException#getCode() error code}. + * @param errorsMap is a map with all validation errors + * @return the corresponding {@link Response}. + */ + protected Response createResponse(Status status, NlsRuntimeException error, String message, String code, + Map> errorsMap) { + + return createResponse(status, message, code, error.getUuid(), errorsMap); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param status is the HTTP {@link Status}. + * @param message is the JSON message attribute. + * @param code is the {@link NlsRuntimeException#getCode() error code}. + * @param uuid the {@link UUID} of the response message. + * @param errorsMap is a map with all validation errors + * @return the corresponding {@link Response}. + */ + protected Response createResponse(Status status, String message, String code, UUID uuid, + Map> errorsMap) { + + String json = createJsonErrorResponseMessage(message, code, uuid, errorsMap); + return Response.status(status).entity(json).build(); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param message the message of the response message + * @param code the code of the response message + * @param uuid the uuid of the response message + * @param errorsMap is a map with all validation errors + * @return the response message as a JSON-String + */ + protected String createJsonErrorResponseMessage(String message, String code, UUID uuid, + Map> errorsMap) { + + Map jsonMap = new HashMap<>(); + if (message != null) { + jsonMap.put(ServiceConstants.KEY_MESSAGE, message); + } + if (code != null) { + jsonMap.put(ServiceConstants.KEY_CODE, code); + } + if (uuid != null) { + jsonMap.put(ServiceConstants.KEY_UUID, uuid.toString()); + } + + if (errorsMap != null) { + jsonMap.put(ServiceConstants.KEY_ERRORS, errorsMap); + } + + String responseMessage = ""; + try { + responseMessage = this.mapper.writeValueAsString(jsonMap); + } catch (JsonProcessingException e) { + LOG.error("Exception facade failed to create JSON.", e); + responseMessage = "{}"; + } + return responseMessage; + + } + + /** + * Add a response message to an existing response. + * + * @param exception the {@link WebApplicationException}. + * @return the response with the response message added + */ + protected Response createResponse(WebApplicationException exception) { + + Response response = exception.getResponse(); + int statusCode = response.getStatus(); + Status status = Status.fromStatusCode(statusCode); + NlsRuntimeException error; + if (exception instanceof ServerErrorException) { + error = new TechnicalErrorUserException(exception); + LOG.error("Service failed on server", error); + return createResponse(status, error, null); + } else { + UUID uuid = UUID.randomUUID(); + if (exception instanceof ClientErrorException) { + LOG.warn("Service failed due to unexpected request. UUDI: {}, reason: {} ", uuid, exception.getMessage()); + } else { + LOG.warn("Service caused redirect or other error. UUID: {}, reason: {}", uuid, exception.getMessage()); + } + return createResponse(status, exception.getMessage(), String.valueOf(statusCode), uuid, null); + } + + } + + /** + * @return the {@link ObjectMapper} for JSON mapping. + */ + public ObjectMapper getMapper() { + + return this.mapper; + } + + /** + * @param mapper the mapper to set + */ + @Inject + public void setMapper(ObjectMapper mapper) { + + this.mapper = mapper; + } + + /** + * @param exposeInternalErrorDetails - {@code true} if internal exception details shall be exposed to clients (useful + * for debugging and testing), {@code false} if such details are hidden to prevent + * Sensitive Data Exposure + * (default, has to be used in production environment). + */ + public void setExposeInternalErrorDetails(boolean exposeInternalErrorDetails) { + + this.exposeInternalErrorDetails = exposeInternalErrorDetails; + if (exposeInternalErrorDetails) { + String message = "****** Exposing of internal error details is enabled! This violates OWASP A6 (Sensitive Data Exposure) and shall only be used for testing/debugging and never in production. ******"; + LOG.warn(message); + // CHECKSTYLE:OFF (for development only) + System.err.println(message); + // CHECKSTYLE:ON + } + } + + /** + * @return exposeInternalErrorDetails the value set by {@link #setExposeInternalErrorDetails(boolean)}. + */ + public boolean isExposeInternalErrorDetails() { + + return this.exposeInternalErrorDetails; + } + +} \ No newline at end of file diff --git a/src/main/java/com/devonfw/quarkus/exceptionutils/ServiceConstants.java b/src/main/java/com/devonfw/quarkus/exceptionutils/ServiceConstants.java new file mode 100644 index 00000000..deabf02d --- /dev/null +++ b/src/main/java/com/devonfw/quarkus/exceptionutils/ServiceConstants.java @@ -0,0 +1,72 @@ +package com.devonfw.quarkus.exceptionutils; + +/** + * Constants for {@link com.devonfw.module.service.common.api.Service}s. + * + * @since 3.0.0 + */ +public class ServiceConstants { + + /** Key for {@link Throwable#getMessage() error message}. */ + public static final String KEY_MESSAGE = "message"; + + /** Key for error {@link java.util.UUID}. */ + public static final String KEY_UUID = "uuid"; + + /** Key for error code. */ + public static final String KEY_CODE = "code"; + + /** Key for HTTP status code. */ + public static final String KEY_STATUS = "status"; + + /** Key for HTTP error. */ + public static final String KEY_ERROR = "error"; + + /** Key for (validation) error details. */ + public static final String KEY_ERRORS = "errors"; + + /** The services URL folder. */ + public static final String URL_FOLDER_SERVICES = "services"; + + /** The services URL path. */ + public static final String URL_PATH_SERVICES = "/" + URL_FOLDER_SERVICES; + + /** The rest URL folder. */ + public static final String URL_FOLDER_REST = "rest"; + + /** The web-service URL folder. */ + public static final String URL_FOLDER_WEB_SERVICE = "ws"; + + /** The rest services URL path. */ + public static final String URL_PATH_REST_SERVICES = URL_PATH_SERVICES + "/" + URL_FOLDER_REST; + + /** The web-service URL path. */ + public static final String URL_PATH_WEB_SERVICES = URL_PATH_SERVICES + "/" + URL_FOLDER_WEB_SERVICE; + + /** + * The variable that resolves to the + * {@link com.devonfw.module.basic.common.api.reflect.Devon4jPackage#getApplication() technical name of the + * application}. + */ + public static final String VARIABLE_APP = "${app}"; + + /** + * The variable that resolves to the + * {@link com.devonfw.module.basic.common.api.reflect.Devon4jPackage#getApplication() technical name of the + * application}. + */ + public static final String VARIABLE_LOCAL_SERVER_PORT = "${local.server.port}"; + + /** + * The variable that resolves to type of the service (e.g. "rest" for REST service and "ws" for SOAP service). + */ + public static final String VARIABLE_TYPE = "${type}"; + + /** + * URL suffix for convention to retrieve WSDL from SOAP service. + * + * @since 2021.04.003 + */ + public static final String WSDL_SUFFIX = "?wsdl"; + +} \ No newline at end of file diff --git a/src/main/java/com/devonfw/quarkus/productmanagement/service/v1/ProductRestService.java b/src/main/java/com/devonfw/quarkus/productmanagement/service/v1/ProductRestService.java index 4cb9c128..49b7fc70 100644 --- a/src/main/java/com/devonfw/quarkus/productmanagement/service/v1/ProductRestService.java +++ b/src/main/java/com/devonfw/quarkus/productmanagement/service/v1/ProductRestService.java @@ -22,6 +22,7 @@ import org.springframework.data.domain.Page; import org.tkit.quarkus.rs.models.PageResultDTO; +import com.devonfw.quarkus.exceptionutils.ProductNotFoundException; import com.devonfw.quarkus.productmanagement.logic.UcFindProduct; import com.devonfw.quarkus.productmanagement.logic.UcManageProduct; import com.devonfw.quarkus.productmanagement.service.v1.model.NewProductDto; @@ -113,9 +114,13 @@ public ProductDto createNewProduct(NewProductDto dto) { @APIResponse(responseCode = "404", description = "Product not found"), @APIResponse(responseCode = "500") }) @Operation(operationId = "getProductById", description = "Returns Product with given id") @GET - @Path("{id}") + @Path("/id/{id}") public ProductDto getProductById(@Parameter(description = "Product unique id") @PathParam("id") String id) { + // A sample scenario to simulate exception mapping + if (Integer.parseInt(id) > 100) { + throw new ProductNotFoundException("Products not found!!"); + } return this.ucFindProduct.findProduct(id); } diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/BaseTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/BaseTest.java new file mode 100644 index 00000000..bdc10d60 --- /dev/null +++ b/src/test/java/com/devonfw/quarkus/exceptionutils/BaseTest.java @@ -0,0 +1,64 @@ +package com.devonfw.quarkus.exceptionutils; + +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; + +public abstract class BaseTest extends Assertions { + + private static boolean initialSetup = false; + + /** + * Initializes this test class and resets {@link #isInitialSetup() initial setup flag}. + */ + @BeforeClass + public static void setUpClass() { + + initialSetup = true; + } + + /** + * Suggests to use {@link #doSetUp()} method before each tests. + */ + @Before + public final void setUp() { + + doSetUp(); + if (initialSetup) { + initialSetup = false; + } + } + + /** + * Suggests to use {@link #doTearDown()} method before each tests. + */ + @After + public final void tearDown() { + + doTearDown(); + } + + /** + * @return {@code true} if this JUnit class is invoked for the first time (first test method is called), {@code false} + * otherwise (if this is a subsequent invocation). + */ + protected boolean isInitialSetup() { + + return initialSetup; + } + + /** + * Provides initialization previous to the creation of the text fixture. + */ + protected void doSetUp() { + + } + + /** + * Provides clean up after tests. + */ + protected void doTearDown() { + + } +} diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/CategoryModuleTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/CategoryModuleTest.java new file mode 100644 index 00000000..974e1108 --- /dev/null +++ b/src/test/java/com/devonfw/quarkus/exceptionutils/CategoryModuleTest.java @@ -0,0 +1,8 @@ +package com.devonfw.quarkus.exceptionutils; + +/** + * This is the JUnit {@link org.junit.experimental.categories.Category} + */ +public interface CategoryModuleTest { + +} diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/ModuleTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/ModuleTest.java new file mode 100644 index 00000000..60b74d6c --- /dev/null +++ b/src/test/java/com/devonfw/quarkus/exceptionutils/ModuleTest.java @@ -0,0 +1,16 @@ +package com.devonfw.quarkus.exceptionutils; + +import org.junit.experimental.categories.Category; + +/** + * This is the abstract base class for a module test. You are free to create your module tests as you like just by + * annotating {@link CategoryModuleTest} using {@link Category}. However, in most cases it will be convenient just to + * extend this class. + * + * @see ModuleTest + * + */ +@Category(CategoryModuleTest.class) +public abstract class ModuleTest extends BaseTest { + +} \ No newline at end of file diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacadeTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacadeTest.java new file mode 100644 index 00000000..5ce2b095 --- /dev/null +++ b/src/test/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacadeTest.java @@ -0,0 +1,281 @@ +package com.devonfw.quarkus.exceptionutils; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.ValidationException; +import javax.validation.Validator; +import javax.validation.constraints.Min; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; + +import net.sf.mmm.util.exception.api.IllegalCaseException; +import net.sf.mmm.util.exception.api.NlsRuntimeException; +import net.sf.mmm.util.exception.api.ObjectNotFoundUserException; +import net.sf.mmm.util.exception.api.TechnicalErrorUserException; +import net.sf.mmm.util.exception.api.ValidationErrorUserException; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Test-case for {@link RestServiceExceptionFacade}. + * + */ +public class RestServiceExceptionFacadeTest extends ModuleTest { + + /** Value of {@link TechnicalErrorUserException#getCode()}. */ + private static final String CODE_TECHNICAL_ERROR = "TechnicalError"; + + /** Placeholder for any UUID. */ + private static final String UUID_ANY = ""; + + /** + * @return the {@link RestServiceExceptionFacade} instance to test. + */ + protected RestServiceExceptionFacade getExceptionFacade() { + + RestServiceExceptionFacade facade = new RestServiceExceptionFacade(); + facade.setMapper(new ObjectMapper()); + return facade; + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with constraint violations + */ + @Test + public void testConstraintViolationExceptions() { + + class CounterTest { + + @Min(value = 10) + private Integer count; + + public CounterTest(Integer count) { + + this.count = count; + } + + } + + CounterTest counter = new CounterTest(new Integer(1)); + + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Set> violations = validator.validate(counter); + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String message = "{count=[" + violations.iterator().next().getMessage() + "]}"; + String errors = "{count=[" + violations.iterator().next().getMessage() + "]}"; + Throwable error = new ConstraintViolationException(violations); + checkFacade(exceptionFacade, error, 400, message, UUID_ANY, ValidationErrorUserException.CODE, errors); + + } + + /** + * Checks that the specified {@link RestServiceExceptionFacade} provides the expected results for the given + * {@link Throwable}. + * + * @param exceptionFacade is the {@link RestServiceExceptionFacade} to test. + * @param error is the {@link Throwable} to convert. + * @param statusCode is the expected {@link Response#getStatus() status} code. + * @param message is the expected {@link Throwable#getMessage() error message} from the JSON result. + * @param uuid is the expected {@link NlsRuntimeException#getUuid() UUID} from the JSON result. May be {@code null}. + * @param code is the expected {@link NlsRuntimeException#getCode() error code} from the JSON result. May be + * {@code null}. + * @return the JSON result for potential further asserts. + */ + protected String checkFacade(RestServiceExceptionFacade exceptionFacade, Throwable error, int statusCode, + String message, String uuid, String code) { + + return checkFacade(exceptionFacade, error, statusCode, message, uuid, code, null); + } + + /** + * Checks that the specified {@link RestServiceExceptionFacade} provides the expected results for the given + * {@link Throwable}. + * + * @param exceptionFacade is the {@link RestServiceExceptionFacade} to test. + * @param error is the {@link Throwable} to convert. + * @param statusCode is the expected {@link Response#getStatus() status} code. + * @param message is the expected {@link Throwable#getMessage() error message} from the JSON result. + * @param uuid is the expected {@link NlsRuntimeException#getUuid() UUID} from the JSON result. May be {@code null}. + * @param code is the expected {@link NlsRuntimeException#getCode() error code} from the JSON result. May be + * {@code null}. + * @param errors is the expected validation errors in a format key-value + * @return the JSON result for potential further asserts. + */ + @SuppressWarnings("unchecked") + protected String checkFacade(RestServiceExceptionFacade exceptionFacade, Throwable error, int statusCode, + String message, String uuid, String code, String errors) { + + Response response = exceptionFacade.toResponse(error); + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(statusCode); + + Object entity = response.getEntity(); + assertThat(entity).isInstanceOf(String.class); + String result = (String) entity; + + try { + Map valueMap = exceptionFacade.getMapper().readValue(result, Map.class); + String msg = message; + if (msg == null) { + msg = error.getMessage(); + } + assertThat(valueMap.get(ServiceConstants.KEY_MESSAGE)).isEqualTo(msg); + if ((statusCode == 403) && (!exceptionFacade.isExposeInternalErrorDetails())) { + assertThat(result).doesNotContain(error.getMessage()); + } + assertThat(valueMap.get(ServiceConstants.KEY_CODE)).isEqualTo(code); + String actualUuid = (String) valueMap.get(ServiceConstants.KEY_UUID); + if (UUID_ANY.equals(uuid)) { + if (actualUuid == null) { + fail("UUID expected but not found in response: " + result); + } + } else { + assertThat(actualUuid).isEqualTo(uuid); + } + + Map> errorsMap = (Map>) valueMap.get(ServiceConstants.KEY_ERRORS); + + if (errors == null) { + if (errorsMap != null) { + fail("Errors do not expected but found in response: " + result); + } else { + assertThat(errorsMap).isEqualTo(errors); + } + } else { + if (errorsMap != null) { + assertThat(errorsMap.toString()).isEqualTo(errors); + } else { + fail("Errors expected but not found in response: " + result); + } + + } + + } catch (Exception e) { + throw new IllegalStateException(e.getMessage(), e); + } + return result; + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testJaxrsInternalServerException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String internalMessage = "The HTTP request is invalid"; + int statusCode = 500; + InternalServerErrorException error = new InternalServerErrorException(internalMessage); + TechnicalErrorUserException technicalErrorUserException = new TechnicalErrorUserException(error); + String expectedMessage = technicalErrorUserException.getMessage(); + checkFacade(exceptionFacade, technicalErrorUserException, statusCode, expectedMessage, + technicalErrorUserException.getUuid().toString(), CODE_TECHNICAL_ERROR); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception. + */ + @Test + public void testJaxrsBadRequestException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String message = "The HTTP request is invalid"; + Throwable error = new BadRequestException(message); + checkFacade(exceptionFacade, error, 400, message, UUID_ANY, "400"); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with a {@link ValidationException}. + */ + @Test + public void testValidationException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String message = "Validation failed!"; + Throwable error = new ValidationException(message); + checkFacade(exceptionFacade, error, 400, message, UUID_ANY, ValidationErrorUserException.CODE); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testJaxrsNotFoundException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String internalMessage = "Either the service URL is wrong or the requested resource does not exist"; + checkFacade(exceptionFacade, new NotFoundException(internalMessage), 404, internalMessage, UUID_ANY, "404"); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testTechnicalJavaRuntimeServerException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String secretMessage = "Internal server error occurred"; + IllegalArgumentException error = new IllegalArgumentException(secretMessage); + TechnicalErrorUserException technicalErrorUserException = new TechnicalErrorUserException(error); + String expectedMessage = technicalErrorUserException.getMessage(); + checkFacade(exceptionFacade, technicalErrorUserException, 500, expectedMessage, + technicalErrorUserException.getUuid().toString(), CODE_TECHNICAL_ERROR); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testTechnicalCustomRuntimeServerException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String message = "Internal server error occurred"; + IllegalCaseException error = new IllegalCaseException(message); + String expectedMessage = new TechnicalErrorUserException(error).getMessage(); + checkFacade(exceptionFacade, error, 500, expectedMessage, error.getUuid().toString(), CODE_TECHNICAL_ERROR); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testTechnicalCustomRuntimeServerExceptionExposed() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + exceptionFacade.setExposeInternalErrorDetails(true); + String message = "Internal server error occurred"; + IllegalCaseException error = new IllegalCaseException(message); + + String expectedMessage = "TechnicalErrorUserException: An unexpected error has occurred! We apologize any inconvenience. Please try again later." + + System.lineSeparator() + error.getUuid().toString() + ":" + CODE_TECHNICAL_ERROR + System.lineSeparator() + + error.getClass().getSimpleName() + ": " + error.getMessage(); + checkFacade(exceptionFacade, error, 500, expectedMessage, error.getUuid().toString(), CODE_TECHNICAL_ERROR); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testBusinessException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + ObjectNotFoundUserException error = new ObjectNotFoundUserException(4711L); + checkFacade(exceptionFacade, error, 400, null, error.getUuid().toString(), "NotFound"); + } +} \ No newline at end of file diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/TagModuleTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/TagModuleTest.java new file mode 100644 index 00000000..5b0c73ef --- /dev/null +++ b/src/test/java/com/devonfw/quarkus/exceptionutils/TagModuleTest.java @@ -0,0 +1,19 @@ +package com.devonfw.quarkus.exceptionutils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Tag; + +/** + * This is the meta Annotation JUnit5 {@link org.junit.jupiter.api.Tag} for a Module Test + * + */ +@Tag("module") +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface TagModuleTest { + +} \ No newline at end of file