From c9526d2389facaf6bbed1582b235dd6af9dc4aa2 Mon Sep 17 00:00:00 2001 From: DeclanClarkeCGI <142809814+DeclanClarkeCGI@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:32:38 +0100 Subject: [PATCH] Po 691 api exception handling (#518) * remove duplicate exception handling * fix column name in entity * change nocontent to notfound * remove unused test class * add auth error handling * add auth error handling * add more global exception handling * api unauthorised test * fix existing tests * add 503 test * change warn to error * remove test container * remove unused test * remove dissimilar assertion that is already tested in Response Util test * update exception http status and test * Remove I_AM_A_TEAPOT exceptions and improve error text generally * api unauthorised test * fix existing tests * add 503 test * remove test container * remove unused test * remove dissimilar assertion that is already tested in Response Util test * update exception http status and test * Remove I_AM_A_TEAPOT exceptions and improve error text generally * merge conflicts * merge conflicts * merge conflicts * more coverage * checkstyle --- .../exception/AuthenticationError.java | 6 +- .../CustomAuthenticationExceptions.java | 2 +- .../advice/GlobalExceptionHandler.java | 147 ++++++++------ .../CustomAuthenticationExceptionsTest.java | 4 +- .../advice/GlobalExceptionHandlerTest.java | 181 +++++++++++------- 5 files changed, 201 insertions(+), 139 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/opal/authentication/exception/AuthenticationError.java b/src/main/java/uk/gov/hmcts/opal/authentication/exception/AuthenticationError.java index 1eb061845..5dccfda08 100644 --- a/src/main/java/uk/gov/hmcts/opal/authentication/exception/AuthenticationError.java +++ b/src/main/java/uk/gov/hmcts/opal/authentication/exception/AuthenticationError.java @@ -11,19 +11,19 @@ public enum AuthenticationError implements OpalApiError { FAILED_TO_OBTAIN_ACCESS_TOKEN( "100", - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.UNAUTHORIZED, "Failed to obtain access token" ), FAILED_TO_VALIDATE_ACCESS_TOKEN( "101", - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.UNAUTHORIZED, "Failed to validate access token" ), FAILED_TO_PARSE_ACCESS_TOKEN( "102", - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.UNAUTHORIZED, "Failed to parse access token" ), diff --git a/src/main/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptions.java b/src/main/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptions.java index 111fa42d2..acee5a1ef 100644 --- a/src/main/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptions.java +++ b/src/main/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptions.java @@ -30,7 +30,7 @@ public void commence(HttpServletRequest request, // Write the custom message to the response body try (PrintWriter writer = response.getWriter()) { writer.write("{\"error\": \"Unauthorized\", \"message\":" - + " \"Unauthorized: request could not be authorized\"}"); + + authException.getMessage() + "}"); } } diff --git a/src/main/java/uk/gov/hmcts/opal/controllers/advice/GlobalExceptionHandler.java b/src/main/java/uk/gov/hmcts/opal/controllers/advice/GlobalExceptionHandler.java index f58a67fb7..f215809ad 100644 --- a/src/main/java/uk/gov/hmcts/opal/controllers/advice/GlobalExceptionHandler.java +++ b/src/main/java/uk/gov/hmcts/opal/controllers/advice/GlobalExceptionHandler.java @@ -13,11 +13,13 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; import org.springframework.transaction.TransactionSystemException; import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import uk.gov.hmcts.opal.authentication.exception.MissingRequestHeaderException; @@ -26,6 +28,7 @@ import uk.gov.hmcts.opal.exception.OpalApiException; import uk.gov.hmcts.opal.launchdarkly.FeatureDisabledException; +import java.util.LinkedHashMap; import java.util.Map; import static uk.gov.hmcts.opal.authentication.service.AccessTokenService.AUTH_HEADER; @@ -36,18 +39,25 @@ @RequiredArgsConstructor public class GlobalExceptionHandler { - public static final String ERROR_MESSAGE = "errorMessage"; + public static final String ERROR = "error"; + + public static final String MESSAGE = "message"; + + public static final String DB_UNAVAILABLE_MESSAGE = "Opal Fines Database is currently unavailable"; + private final AccessTokenService tokenService; @ExceptionHandler(FeatureDisabledException.class) public ResponseEntity handleFeatureDisabledException(FeatureDisabledException ex) { - return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(ex.getMessage()); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) + .contentType(MediaType.APPLICATION_JSON).body(ex.getMessage()); } @ExceptionHandler(MissingRequestHeaderException.class) public ResponseEntity handleMissingRequestHeaderException(MissingRequestHeaderException ex) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON).body(ex.getMessage()); } @ExceptionHandler({PermissionNotAllowedException.class, AccessDeniedException.class}) @@ -55,9 +65,10 @@ public ResponseEntity handlePermissionNotAllowedException(Exception ex, HttpServletRequest request) { String authorization = request.getHeader(AUTH_HEADER); String preferredName = extractPreferredUsername(authorization, tokenService); - String message = String.format("For user %s, %s", preferredName, ex.getMessage()); + String message = String.format("{\"error\": \"Forbidden\", \"message\" : \"For user %s, %s \"}", preferredName, + ex.getMessage()); log.error(message); - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(message); + return ResponseEntity.status(HttpStatus.FORBIDDEN).contentType(MediaType.APPLICATION_JSON).body(message); } @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) @@ -67,33 +78,37 @@ public ResponseEntity> handleHttpMediaTypeNotAcceptableExcep log.error(":handleHttpMediaTypeNotAcceptableException: {}", ex.getMessage()); log.error(":handleHttpMediaTypeNotAcceptableException:", ex.getCause()); - Map body = Map.of( - "error", "Not Acceptable", - "message", "The server cannot produce a response matching the request Accept header" - ); - return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, "Not Acceptable"); + body.put(MESSAGE, ex.getMessage() + ", " + ex.getBody().getDetail()); + + return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).contentType(MediaType.APPLICATION_JSON).body(body); } @ExceptionHandler public ResponseEntity> handlePropertyValueException(PropertyValueException pve) { log.error(":handlePropertyValueException: {}", pve.getMessage()); - Map body = Map.of( - ERROR_MESSAGE, pve.getMessage(), - "entity", pve.getEntityName(), - "property", pve.getPropertyName() - ); - return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, pve.getMessage()); + body.put("entity", pve.getEntityName()); + body.put("property", pve.getPropertyName()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON).body(body); } - @ExceptionHandler + @ExceptionHandler({HttpMediaTypeNotSupportedException.class, HttpMessageNotReadableException.class}) public ResponseEntity> handleHttpMessageNotReadableException( - HttpMessageNotReadableException hmnre) { + Exception ex) { + + log.error(":handleHttpMessageNotReadableException: {}", ex.getMessage()); + Map body = new LinkedHashMap<>(); + body.put(ERROR, ex.getMessage()); + body.put(MESSAGE, + "The request body could not be read, ensure content-type is application/json"); - log.error(":handleHttpMessageNotReadableException: {}", hmnre.getMessage()); - Map body = Map.of( - ERROR_MESSAGE, hmnre.getMessage() - ); - return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).body(body); + return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + .contentType(MediaType.APPLICATION_JSON).body(body); } @ExceptionHandler @@ -101,10 +116,12 @@ public ResponseEntity> handleInvalidDataAccessApiUsageExcept InvalidDataAccessApiUsageException idaaue) { log.error(":handleInvalidDataAccessApiUsageException: {}", idaaue.getMessage()); - Map body = Map.of( - ERROR_MESSAGE, idaaue.getMessage() - ); - return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).body(body); + + Map body = new LinkedHashMap<>(); + + body.put(ERROR, idaaue.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON).body(body); } @ExceptionHandler @@ -114,10 +131,11 @@ public ResponseEntity> handleInvalidDataAccessResourceUsageE log.error(":handleInvalidDataAccessApiUsageException: {}", idarue.getMessage()); log.error(":handleInvalidDataAccessApiUsageException:", idarue.getRootCause()); - Map body = Map.of( - ERROR_MESSAGE, idarue.getMessage() - ); - return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, idarue.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON).body(body); } @ExceptionHandler @@ -127,10 +145,10 @@ public ResponseEntity> handleEntityNotFoundException( log.error(":handleEntityNotFoundException: {}", entityNotFoundException.getMessage()); log.error(":handleEntityNotFoundException:", entityNotFoundException.getCause()); - Map body = Map.of( - ERROR_MESSAGE, entityNotFoundException.getMessage() - ); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, "Entity Not Found"); + body.put(MESSAGE, entityNotFoundException.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).contentType(MediaType.APPLICATION_JSON).body(body); } @ExceptionHandler(OpalApiException.class) @@ -140,11 +158,11 @@ public ResponseEntity> handleOpalApiException( log.error(":handleOpalApiException: {}", opalApiException.getMessage()); log.error(":handleOpalApiException:", opalApiException.getCause()); - Map body = Map.of( - "error", opalApiException.getError().getHttpStatus().getReasonPhrase(), - "message", opalApiException.getMessage() - ); - return ResponseEntity.status(opalApiException.getError().getHttpStatus()).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, opalApiException.getError().getHttpStatus().getReasonPhrase()); + body.put(MESSAGE, opalApiException.getMessage()); + return ResponseEntity.status(opalApiException.getError().getHttpStatus()) + .contentType(MediaType.APPLICATION_JSON).body(body); } @ExceptionHandler({ServletException.class, TransactionSystemException.class, PersistenceException.class}) @@ -153,19 +171,18 @@ public ResponseEntity> handleDatabaseExceptions(Exception ex if (ex instanceof QueryTimeoutException) { log.error(":handleQueryTimeoutException: {}", ex.getMessage()); - Map body = Map.of( - "error", "Request Timeout", - "message", "The request did not receive a response from the database within the timeout period" - ); - return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, "Request Timeout"); + body.put(MESSAGE, "The request did not receive a response from the database within the timeout period"); + return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).contentType(MediaType.APPLICATION_JSON).body(body); } // If it's not a QueryTimeoutException, return a generic internal server error - Map body = Map.of( - "error", "Internal Server Error", - "message", "An unexpected error occurred" - ); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, "Internal Server Error"); + body.put(MESSAGE, "An unexpected error occurred"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON).body(body); } @ExceptionHandler @@ -176,17 +193,19 @@ public ResponseEntity> handlePsqlException( log.error(":handlePSQLException:", psqlException.getCause()); if (psqlException.getCause() instanceof java.net.ConnectException) { - Map body = Map.of( - "error", "Service Unavailable", "message", - "Opal Fines Database is currently unavailable" - ); - return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, "Service Unavailable"); + body.put(MESSAGE, DB_UNAVAILABLE_MESSAGE); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) + .contentType(MediaType.APPLICATION_JSON).body(body); } - Map body = Map.of( - "error", "Internal Server Error", "message", psqlException.getMessage() - ); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, "Internal Server Error"); + body.put(MESSAGE, psqlException.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON).body(body); } @ExceptionHandler @@ -196,9 +215,11 @@ public ResponseEntity> handleDataAccessResourceFailureExcept log.error(":handleDataAccessResourceFailureException: {}", dataAccessResourceFailureException.getMessage()); log.error(":handleDataAccessResourceFailureException:", dataAccessResourceFailureException.getCause()); - Map body = Map.of( - "error", "Service Unavailable", "message", "Opal Fines Database is currently unavailable" - ); - return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(body); + Map body = new LinkedHashMap<>(); + body.put(ERROR, "Service Unavailable"); + body.put(MESSAGE, DB_UNAVAILABLE_MESSAGE); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).contentType(MediaType.APPLICATION_JSON).body(body); } + + } diff --git a/src/test/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptionsTest.java b/src/test/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptionsTest.java index cf1d9c5ee..a17e48315 100644 --- a/src/test/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptionsTest.java +++ b/src/test/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptionsTest.java @@ -39,8 +39,8 @@ void commenceShouldReturnUnauthorizedResponse() throws IOException, ServletExcep verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); verify(response).setContentType("application/json"); - verify(writer).write("{\"error\": \"Unauthorized\", \"message\": " - + "\"Unauthorized: request could not be authorized\"}"); + verify(writer).write("{\"error\": \"Unauthorized\", \"message\":" + + authException.getMessage() + "}"); } @Test diff --git a/src/test/java/uk/gov/hmcts/opal/controllers/advice/GlobalExceptionHandlerTest.java b/src/test/java/uk/gov/hmcts/opal/controllers/advice/GlobalExceptionHandlerTest.java index 8ef6f7080..a13d34f17 100644 --- a/src/test/java/uk/gov/hmcts/opal/controllers/advice/GlobalExceptionHandlerTest.java +++ b/src/test/java/uk/gov/hmcts/opal/controllers/advice/GlobalExceptionHandlerTest.java @@ -1,21 +1,23 @@ package uk.gov.hmcts.opal.controllers.advice; import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceException; +import jakarta.persistence.QueryTimeoutException; import jakarta.servlet.http.HttpServletRequest; import org.hibernate.PropertyValueException; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.access.AccessDeniedException; import org.springframework.test.context.ContextConfiguration; import org.springframework.web.HttpMediaTypeNotAcceptableException; import uk.gov.hmcts.opal.authentication.exception.AuthenticationError; @@ -26,10 +28,10 @@ import uk.gov.hmcts.opal.exception.OpalApiException; import uk.gov.hmcts.opal.launchdarkly.FeatureDisabledException; +import java.net.ConnectException; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.when; @SpringBootTest @ContextConfiguration(classes = GlobalExceptionHandler.class) @@ -51,32 +53,21 @@ class GlobalExceptionHandlerTest { GlobalExceptionHandler globalExceptionHandler; @Test - void handleFeatureDisabledException_ReturnsMethodNotAllowed() { - // Arrange - String errorMessage = "Feature is disabled"; - when(exception.getMessage()).thenReturn(errorMessage); - - // Act + void testHandleFeatureDisabledException() { + FeatureDisabledException exception = new FeatureDisabledException("Feature is disabled"); ResponseEntity response = globalExceptionHandler.handleFeatureDisabledException(exception); - // Assert assertEquals(HttpStatus.METHOD_NOT_ALLOWED, response.getStatusCode()); - assertEquals(errorMessage, response.getBody()); + assertEquals("Feature is disabled", response.getBody()); } @Test - void handleMissingRequestHeaderException_ReturnsBadRequest() { - // Arrange - String errorMessage = "Missing required header"; - when(missingRequestHeaderException.getMessage()).thenReturn(errorMessage); - - // Act - ResponseEntity response = globalExceptionHandler.handleMissingRequestHeaderException( - missingRequestHeaderException); + void testHandleMissingRequestHeaderException() { + MissingRequestHeaderException exception = new MissingRequestHeaderException("TYPE"); + ResponseEntity response = globalExceptionHandler.handleMissingRequestHeaderException(exception); - // Assert assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertEquals(errorMessage, response.getBody()); + assertEquals("Missing request header named: TYPE", response.getBody()); } @Test @@ -93,83 +84,133 @@ void handlePermissionNotAllowedException_ShouldReturnForbiddenResponse() { } @Test - void handleAccessDeniedException_ShouldReturnForbiddenResponse() { - // Arrange - AccessDeniedException ex = new AccessDeniedException("access denied"); - HttpServletRequest request = new MockHttpServletRequest(); - // Act - ResponseEntity response = globalExceptionHandler.handlePermissionNotAllowedException(ex, request); + void testHandleHttpMediaTypeNotAcceptableException() { + HttpMediaTypeNotAcceptableException exception = new HttpMediaTypeNotAcceptableException("Not acceptable"); + ResponseEntity> response = globalExceptionHandler + .handleHttpMediaTypeNotAcceptableException(exception); - // Assert - assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + assertEquals(HttpStatus.NOT_ACCEPTABLE, response.getStatusCode()); + assertEquals("Not Acceptable", response.getBody().get("error")); + assertEquals("Not acceptable, Could not parse Accept header.", + response.getBody().get("message")); } @Test - void handlePropertyValueException() { - // Arrange - PropertyValueException pve = new PropertyValueException("A Test Message", "DraftAccountEntity", "account"); - // Act - ResponseEntity> response = globalExceptionHandler.handlePropertyValueException(pve); - // Assert - assertEquals(org.htmlunit.http.HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); + void testHandlePropertyValueException() { + PropertyValueException exception = new PropertyValueException("Property value exception", "entity", + "property"); + ResponseEntity> response = globalExceptionHandler.handlePropertyValueException(exception); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertEquals("Property value exception : entity.property", response.getBody().get("error")); } @Test - void handleHttpMessageNotReadableException() { - // Arrange - HttpInputMessage input = Mockito.mock(HttpInputMessage.class); - HttpMessageNotReadableException hmnre = new HttpMessageNotReadableException("A Test Message", input); - // Act + void testHandleHttpMessageNotReadableException() { + HttpMessageNotReadableException exception = new HttpMessageNotReadableException("Cannot read message", + new Throwable("Root cause")); ResponseEntity> response = globalExceptionHandler - .handleHttpMessageNotReadableException(hmnre); - // Assert - assertEquals(org.htmlunit.http.HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); + .handleHttpMessageNotReadableException(exception); + + assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, response.getStatusCode()); + assertEquals("Cannot read message", response.getBody().get("error")); + assertEquals("The request body could not be read, ensure content-type is application/json", + response.getBody().get("message")); } @Test - void handleInvalidDataAccessApiUsageException() { - // Arrange - InvalidDataAccessApiUsageException idaaue = new InvalidDataAccessApiUsageException("A Test Message"); - // Act + void testHandleInvalidDataAccessApiUsageException() { + InvalidDataAccessApiUsageException exception = + new InvalidDataAccessApiUsageException("Invalid API usage", new Throwable("Root cause")); ResponseEntity> response = globalExceptionHandler - .handleInvalidDataAccessApiUsageException(idaaue); - // Assert - assertEquals(org.htmlunit.http.HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); + .handleInvalidDataAccessApiUsageException(exception); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertEquals("Invalid API usage", response.getBody().get("error")); } @Test - void handleInvalidDataAccessResourceUsageException() { - // Arrange - InvalidDataAccessResourceUsageException idarue = new InvalidDataAccessResourceUsageException("A Test Message"); - // Act + void handleInvalidDataAccessResourceUsageException_ShouldReturnInternalServerError() { + InvalidDataAccessResourceUsageException exception = + new InvalidDataAccessResourceUsageException("Invalid resource usage"); ResponseEntity> response = globalExceptionHandler - .handleInvalidDataAccessResourceUsageException(idarue); - // Assert - assertEquals(org.htmlunit.http.HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); + .handleInvalidDataAccessResourceUsageException(exception); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertEquals("Invalid resource usage", response.getBody().get("error")); } @Test - void handleEntityNotFoundException_ReturnsNotFound() { - EntityNotFoundException ex = new EntityNotFoundException("Entity not found"); - ResponseEntity> response = globalExceptionHandler.handleEntityNotFoundException(ex); + void testHandleEntityNotFoundException() { + EntityNotFoundException exception = new EntityNotFoundException("Entity not found"); + ResponseEntity> response = globalExceptionHandler.handleEntityNotFoundException(exception); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); - assertEquals("Entity not found", response.getBody().get(GlobalExceptionHandler.ERROR_MESSAGE)); + assertEquals("Entity Not Found", response.getBody().get("error")); } @Test void handleOpalApiException_ReturnsInternalServerError() { - OpalApiException ex = new OpalApiException(AuthenticationError.FAILED_TO_PARSE_ACCESS_TOKEN, - "Internal Server Error"); + OpalApiException ex = new OpalApiException( + AuthenticationError.FAILED_TO_OBTAIN_AUTHENTICATION_CONFIG); ResponseEntity> response = globalExceptionHandler.handleOpalApiException(ex); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); - assertEquals("Failed to parse access token. Internal Server Error", response.getBody().get("message")); + assertEquals("Failed to find authentication configuration", response.getBody().get("message")); } @Test - void handleHttpMediaTypeNotAcceptableException_ReturnsNotAcceptable() { - HttpMediaTypeNotAcceptableException ex = new HttpMediaTypeNotAcceptableException("Not acceptable"); + void testHandleDatabaseExceptions_queryTimeout() { + QueryTimeoutException exception = new QueryTimeoutException("Query timeout", null, null); + ResponseEntity> response = globalExceptionHandler.handleDatabaseExceptions(exception); + + assertEquals(HttpStatus.REQUEST_TIMEOUT, response.getStatusCode()); + assertEquals("Request Timeout", response.getBody().get("error")); + assertEquals("The request did not receive a response from the database within the timeout period", + response.getBody().get("message")); + } + + @Test + void handleDatabaseExceptions_OtherDatabaseException_ShouldReturnInternalServerError() { + PersistenceException exception = new PersistenceException("Persistence exception"); + ResponseEntity> response = globalExceptionHandler.handleDatabaseExceptions(exception); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertEquals("Internal Server Error", response.getBody().get("error")); + assertEquals("An unexpected error occurred", response.getBody().get("message")); + } + + @Test + void testHandlePsqlException_serviceUnavailable() { + PSQLException exception = new PSQLException("PSQL Exception", + PSQLState.CONNECTION_FAILURE, + new ConnectException("Connection refused")); + ResponseEntity> response = globalExceptionHandler.handlePsqlException(exception); + + assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.getStatusCode()); + assertEquals("Service Unavailable", response.getBody().get("error")); + assertEquals("Opal Fines Database is currently unavailable", response.getBody().get("message")); + } + + @Test + void handlePsqlException_WithOtherCause_ShouldReturnInternalServerError() { + PSQLException exception = new PSQLException("PSQL Exception", PSQLState.UNEXPECTED_ERROR, + new Throwable("Unexpected error")); + ResponseEntity> response = globalExceptionHandler.handlePsqlException(exception); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertEquals("Internal Server Error", response.getBody().get("error")); + assertEquals("PSQL Exception", response.getBody().get("message")); + } + + @Test + void testHandleDataAccessResourceFailureException() { + DataAccessResourceFailureException exception = + new DataAccessResourceFailureException("Data access resource failure"); ResponseEntity> response = globalExceptionHandler - .handleHttpMediaTypeNotAcceptableException(ex); - assertEquals(HttpStatus.NOT_ACCEPTABLE, response.getStatusCode()); + .handleDataAccessResourceFailureException(exception); + + assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.getStatusCode()); + assertEquals("Service Unavailable", response.getBody().get("error")); + assertEquals("Opal Fines Database is currently unavailable", response.getBody().get("message")); } }