From 7ca5d818513c3c0440f7c743447be15abf8990bd Mon Sep 17 00:00:00 2001 From: DeclanClarkeCGI <142809814+DeclanClarkeCGI@users.noreply.github.com> Date: Wed, 4 Sep 2024 06:38:47 +0100 Subject: [PATCH] Po 690 api exception handling (#516) * 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 --- ...BusinessUnitControllerIntegrationTest.java | 4 +- .../CourtControllerIntegrationTest.java | 4 +- ...ndantAccountControllerIntegrationTest.java | 6 +- ...DraftAccountControllerIntegrationTest.java | 65 ++++++- .../EnforcerControllerIntegrationTest.java | 4 +- ...lJusticeAreaControllerIntegrationTest.java | 4 +- ...ajorCreditorControllerIntegrationTest.java | 4 +- .../OffenceControllerIntegrationTest.java | 4 +- .../ResultControllerIntegrationTest.java | 4 +- .../TestingSupportControllerTest.java | 27 +++ ...gurationItemControllerIntegrationTest.java | 4 +- .../ImpositionControllerIntegrationTest.java | 4 +- .../LogActionControllerIntegrationTest.java | 4 +- ...gAuditDetailControllerIntegrationTest.java | 4 +- .../TillControllerIntegrationTest.java | 4 +- .../authentication/config/SecurityConfig.java | 13 +- .../CustomAuthenticationExceptions.java | 55 ++++++ .../ExceptionControllerAdvice.java | 66 ------- .../advice/GlobalExceptionHandler.java | 161 +++++++++++++++++- .../exception/DataLookUpError.java | 27 +++ .../hmcts/opal/entity/DraftAccountEntity.java | 2 +- .../java/uk/gov/hmcts/opal/util/HttpUtil.java | 25 ++- .../CustomAuthenticationExceptionsTest.java | 57 +++++++ .../DefendantAccountControllerTest.java | 20 ++- .../ExceptionControllerAdviceTest.java | 70 -------- .../advice/GlobalExceptionHandlerTest.java | 81 +++++++++ .../develop/NoteControllerTest.java | 7 +- .../gov/hmcts/opal/util/ResponseUtilTest.java | 19 ++- 28 files changed, 559 insertions(+), 190 deletions(-) create mode 100644 src/main/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptions.java delete mode 100644 src/main/java/uk/gov/hmcts/opal/controllers/ExceptionControllerAdvice.java create mode 100644 src/main/java/uk/gov/hmcts/opal/controllers/exception/DataLookUpError.java create mode 100644 src/test/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptionsTest.java delete mode 100644 src/test/java/uk/gov/hmcts/opal/controllers/ExceptionControllerAdviceTest.java diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/BusinessUnitControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/BusinessUnitControllerIntegrationTest.java index 6a35e70b..cb022af4 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/BusinessUnitControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/BusinessUnitControllerIntegrationTest.java @@ -69,7 +69,7 @@ void testGetBusinessUnitById_WhenBusinessUnitDoesNotExist() throws Exception { when(businessUnitService.getBusinessUnit((short)2)).thenReturn(null); mockMvc.perform(get("/api/business-unit/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -98,7 +98,7 @@ void testPostBusinessUnitsSearch_WhenBusinessUnitDoesNotExist() throws Exception mockMvc.perform(post("/api/business-unit/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } @Test diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/CourtControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/CourtControllerIntegrationTest.java index 14994e04..d1b5ba5d 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/CourtControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/CourtControllerIntegrationTest.java @@ -69,7 +69,7 @@ void testGetCourtById_WhenCourtDoesNotExist() throws Exception { when(courtService.getCourt(2L)).thenReturn(null); mockMvc.perform(get("/api/court/2").header("authorization", "Bearer some_value")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -103,7 +103,7 @@ void testPostCourtsSearch_WhenCourtDoesNotExist() throws Exception { .header("authorization", "Bearer some_value") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } @Test diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DefendantAccountControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DefendantAccountControllerIntegrationTest.java index 47e4d72b..5007e44a 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DefendantAccountControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DefendantAccountControllerIntegrationTest.java @@ -80,7 +80,7 @@ void testGetDefendantAccountById_WhenDefendantAccountDoesNotExist() throws Excep mockMvc.perform(get("/api/defendant-account/2") .header("authorization", "Bearer some_value")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -118,7 +118,7 @@ void testPostDefendantAccountsSearch_WhenDefendantAccountDoesNotExist() throws E .header("authorization", "Bearer some_value") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -217,7 +217,7 @@ public void testGetNotesForDefendantAccount_zeroNotes() throws Exception { mockMvc.perform(get("/api/defendant-account/notes/{defendantId}", "dummyDefendantId") .header("authorization", "Bearer some_value")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java index 8951d190..73d957db 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java @@ -1,6 +1,10 @@ package uk.gov.hmcts.opal.controllers; +import jakarta.persistence.QueryTimeoutException; 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.beans.factory.annotation.Qualifier; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -11,6 +15,8 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import uk.gov.hmcts.opal.authentication.service.AccessTokenService; +import uk.gov.hmcts.opal.controllers.advice.GlobalExceptionHandler; import uk.gov.hmcts.opal.dto.ToJsonString; import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; import uk.gov.hmcts.opal.entity.BusinessUnitEntity; @@ -19,12 +25,14 @@ import uk.gov.hmcts.opal.service.opal.JsonSchemaValidationService; import uk.gov.hmcts.opal.service.opal.UserStateService; +import java.net.ConnectException; import java.time.LocalDate; import java.util.logging.Logger; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -33,7 +41,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest -@ContextConfiguration(classes = DraftAccountController.class) +@ContextConfiguration(classes = {DraftAccountController.class, GlobalExceptionHandler.class}) @ActiveProfiles({"integration"}) class DraftAccountControllerIntegrationTest { @@ -49,6 +57,9 @@ class DraftAccountControllerIntegrationTest { @MockBean UserStateService userStateService; + @MockBean + AccessTokenService tokenService; + @SpyBean private JsonSchemaValidationService jsonSchemaValidationService; @@ -82,7 +93,7 @@ void testGetDraftAccountById_WhenDraftAccountDoesNotExist() throws Exception { when(draftAccountService.getDraftAccount(2L)).thenReturn(null); mockMvc.perform(get("/api/draft-account/2").header("authorization", "Bearer some_value")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -111,7 +122,34 @@ void testPostDraftAccountsSearch_WhenDraftAccountDoesNotExist() throws Exception .header("authorization", "Bearer some_value") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); + } + + @Test + void shouldReturn408WhenTimeout() throws Exception { + // Simulating a timeout exception when the repository is called + doThrow(new QueryTimeoutException()).when(draftAccountService).getDraftAccount(1L); + + mockMvc.perform(get("/api/draft-account/1") + .header("Authorization", "Bearer " + "some_value")) + .andExpect(status().isRequestTimeout()) + .andExpect(content().contentType("application/json")) + .andExpect(content().json(""" + { + "error": "Request Timeout", + "message": "The request did not receive a response from the database within the timeout period" + }""")); + } + + @Test + void shouldReturn406WhenResponseContentTypeNotSupported() throws Exception { + + when(draftAccountService.getDraftAccount(1L)).thenReturn(createDraftAccountEntity()); + + mockMvc.perform(get("/api/draft-account/1") + .header("Authorization", "Bearer " + "some_value") + .accept("application/xml")) + .andExpect(status().isNotAcceptable()); } private DraftAccountEntity createDraftAccountEntity() { @@ -127,4 +165,25 @@ private DraftAccountEntity createDraftAccountEntity() { .timelineData("{}") .build(); } + + @Test + void shouldReturn503WhenDownstreamServiceIsUnavailable() throws Exception { + + Mockito.doAnswer( + invocation -> { + throw new PSQLException("Connection refused", PSQLState.CONNECTION_FAILURE, new ConnectException()); + }) + .when(draftAccountService).getDraftAccount(1L); + + + mockMvc.perform(get("/api/draft-account/1") + .header("Authorization", "Bearer " + "some_value")) + .andExpect(status().isServiceUnavailable()) + .andExpect(content().contentType("application/json")) + .andExpect(content().json(""" + { + "error": "Service Unavailable", + "message": "Opal Fines Database is currently unavailable" + }""")); + } } diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/EnforcerControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/EnforcerControllerIntegrationTest.java index 25678230..0d2a6f01 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/EnforcerControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/EnforcerControllerIntegrationTest.java @@ -67,7 +67,7 @@ void testGetEnforcerById_WhenEnforcerDoesNotExist() throws Exception { when(enforcerService.getEnforcer(2L)).thenReturn(null); mockMvc.perform(get("/api/enforcer/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -102,7 +102,7 @@ void testPostEnforcersSearch_WhenEnforcerDoesNotExist() throws Exception { mockMvc.perform(post("/api/enforcer/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } @Test diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/LocalJusticeAreaControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/LocalJusticeAreaControllerIntegrationTest.java index 0c44f0d9..5d42689c 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/LocalJusticeAreaControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/LocalJusticeAreaControllerIntegrationTest.java @@ -57,7 +57,7 @@ void testGetLocalJusticeAreaById_WhenLocalJusticeAreaDoesNotExist() throws Excep when(localJusticeAreaService.getLocalJusticeArea((short)2)).thenReturn(null); mockMvc.perform(get("/api/local-justice-area/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -85,7 +85,7 @@ void testPostLocalJusticeAreasSearch_WhenLocalJusticeAreaDoesNotExist() throws E mockMvc.perform(post("/api/local-justice-area/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } private LocalJusticeAreaEntity createLocalJusticeAreaEntity() { diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/MajorCreditorControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/MajorCreditorControllerIntegrationTest.java index 2357d63c..a03677cf 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/MajorCreditorControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/MajorCreditorControllerIntegrationTest.java @@ -60,7 +60,7 @@ void testGetMajorCreditorById_WhenMajorCreditorDoesNotExist() throws Exception { when(majorCreditorService.getMajorCreditor(2L)).thenReturn(null); mockMvc.perform(get("/api/major-creditor/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -89,7 +89,7 @@ void testPostMajorCreditorsSearch_WhenMajorCreditorDoesNotExist() throws Excepti mockMvc.perform(post("/api/major-creditor/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } @Test diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/OffenceControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/OffenceControllerIntegrationTest.java index 399b0252..7db5cfaa 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/OffenceControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/OffenceControllerIntegrationTest.java @@ -56,7 +56,7 @@ void testGetOffenceById_WhenOffenceDoesNotExist() throws Exception { when(offenceService.getOffence((short)2)).thenReturn(null); mockMvc.perform(get("/api/offence/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -81,7 +81,7 @@ void testPostOffencesSearch_WhenOffenceDoesNotExist() throws Exception { mockMvc.perform(post("/api/offence/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } private OffenceEntity createOffenceEntity() { diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/ResultControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/ResultControllerIntegrationTest.java index d24130a8..314775ba 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/ResultControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/ResultControllerIntegrationTest.java @@ -75,7 +75,7 @@ void testGetResultById_WhenResultDoesNotExist() throws Exception { when(resultService.getResult(2L)).thenReturn(null); mockMvc.perform(get("/api/result/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -119,7 +119,7 @@ void testPostResultsSearch_WhenResultDoesNotExist() throws Exception { mockMvc.perform(post("/api/result/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/TestingSupportControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/TestingSupportControllerTest.java index 5f8886ba..9f208397 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/TestingSupportControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/TestingSupportControllerTest.java @@ -178,4 +178,31 @@ void testParseToken() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$").value("testUser")); } + + @Test + void testGetTokenForUserFailure() throws Exception { + AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); + accessTokenResponse.setAccessToken("testAccessToken"); + + when(accessTokenService.getTestUserToken(anyString())).thenReturn(accessTokenResponse); + + SecurityToken securityToken = SecurityToken.builder() + .accessToken(TEST_TOKEN) + .userState(USER_STATE) + .build(); + when(authorisationService.getSecurityToken("testAccessToken")).thenReturn(securityToken); + + mockMvc.perform(get("/api/testing-support/token/user") + .header("X-User-Email", "test@example.com")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.accessToken").value("testToken")) + .andExpect(jsonPath("$.userState.userName").value("name")) + .andExpect(jsonPath("$.userState.userId").value("123")) + .andExpect(jsonPath("$.userState.roles[0].businessUnitId").value("123")) + .andExpect(jsonPath("$.userState.roles[0].businessUserId").value("BU123")) + .andExpect(jsonPath("$.userState.roles[0].permissions[0].permissionId").value("1")) + .andExpect(jsonPath("$.userState.roles[0].permissions[0].permissionName") + .value("Notes")); + } } diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/ConfigurationItemControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/ConfigurationItemControllerIntegrationTest.java index 6b185d01..effb7797 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/ConfigurationItemControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/ConfigurationItemControllerIntegrationTest.java @@ -58,7 +58,7 @@ void testGetConfigurationItemById_WhenConfigurationItemDoesNotExist() throws Exc when(configurationItemService.getConfigurationItem(2L)).thenReturn(null); mockMvc.perform(get("/dev/configuration-item/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -84,7 +84,7 @@ void testPostConfigurationItemsSearch_WhenConfigurationItemDoesNotExist() throws mockMvc.perform(post("/dev/configuration-item/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } private ConfigurationItemEntity createConfigurationItemEntity() { diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/ImpositionControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/ImpositionControllerIntegrationTest.java index 4e967e70..c0e97752 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/ImpositionControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/ImpositionControllerIntegrationTest.java @@ -65,7 +65,7 @@ void testGetImpositionById_WhenImpositionDoesNotExist() throws Exception { when(impositionService.getImposition(2L)).thenReturn(null); mockMvc.perform(get("/dev/imposition/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -94,7 +94,7 @@ void testPostImpositionsSearch_WhenImpositionDoesNotExist() throws Exception { mockMvc.perform(post("/dev/imposition/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } private ImpositionEntity createImpositionEntity() { diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/LogActionControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/LogActionControllerIntegrationTest.java index 3e867fb7..c6d642c7 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/LogActionControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/LogActionControllerIntegrationTest.java @@ -53,7 +53,7 @@ void testGetLogActionById_WhenLogActionDoesNotExist() throws Exception { when(logActionService.getLogAction((short)2)).thenReturn(null); mockMvc.perform(get("/dev/log-action/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -77,7 +77,7 @@ void testPostLogActionsSearch_WhenLogActionDoesNotExist() throws Exception { mockMvc.perform(post("/dev/log-action/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } private LogActionEntity createLogActionEntity() { diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/LogAuditDetailControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/LogAuditDetailControllerIntegrationTest.java index d530b051..ff2ee6a8 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/LogAuditDetailControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/LogAuditDetailControllerIntegrationTest.java @@ -61,7 +61,7 @@ void testGetLogAuditDetailById_WhenLogAuditDetailDoesNotExist() throws Exception when(logAuditDetailService.getLogAuditDetail(2L)).thenReturn(null); mockMvc.perform(get("/dev/log-audit-detail/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -89,7 +89,7 @@ void testPostLogAuditDetailsSearch_WhenLogAuditDetailDoesNotExist() throws Excep mockMvc.perform(post("/dev/log-audit-detail/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } private LogAuditDetailEntity createLogAuditDetailEntity() { diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/TillControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/TillControllerIntegrationTest.java index 0b256093..7a6977a9 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/TillControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/develop/TillControllerIntegrationTest.java @@ -56,7 +56,7 @@ void testGetTillById_WhenTillDoesNotExist() throws Exception { when(tillService.getTill(2L)).thenReturn(null); mockMvc.perform(get("/dev/till/2")) - .andExpect(status().isNoContent()); + .andExpect(status().isNotFound()); } @Test @@ -81,7 +81,7 @@ void testPostTillsSearch_WhenTillDoesNotExist() throws Exception { mockMvc.perform(post("/dev/till/search") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) - .andExpect(status().isNoContent()); + .andExpect(status().isOk()); } private TillEntity createTillEntity() { diff --git a/src/main/java/uk/gov/hmcts/opal/authentication/config/SecurityConfig.java b/src/main/java/uk/gov/hmcts/opal/authentication/config/SecurityConfig.java index f58e1b74..062a52d4 100644 --- a/src/main/java/uk/gov/hmcts/opal/authentication/config/SecurityConfig.java +++ b/src/main/java/uk/gov/hmcts/opal/authentication/config/SecurityConfig.java @@ -24,7 +24,9 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.web.filter.OncePerRequestFilter; import uk.gov.hmcts.opal.authentication.config.internal.InternalAuthConfigurationProperties; import uk.gov.hmcts.opal.authentication.config.internal.InternalAuthConfigurationPropertiesStrategy; @@ -49,6 +51,8 @@ public class SecurityConfig { private final InternalAuthConfigurationPropertiesStrategy fallbackConfiguration; private final InternalAuthConfigurationProperties internalAuthConfigurationProperties; private final InternalAuthProviderConfigurationProperties internalAuthProviderConfigurationProperties; + private final AuthenticationEntryPoint customAuthenticationEntryPoint; + private final AccessDeniedHandler customAccessDeniedHandler; private static final String[] AUTH_WHITELIST = { "/swagger-ui.html", @@ -78,8 +82,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .permitAll() .requestMatchers(AUTH_WHITELIST) .permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(exceptionHandling -> + exceptionHandling + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) ) - .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(jwtIssuerAuthenticationManagerResolver()) ); @@ -132,7 +141,7 @@ protected void doFilterInternal(HttpServletRequest request, filterChain.doFilter(request, response); return; } - + log.warn(".AuthorisationTokenExistenceFilter:doFilterInternal: No Bearer Token."); throw new OpalApiException(AuthenticationError.FAILED_TO_OBTAIN_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 new file mode 100644 index 00000000..111fa42d --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptions.java @@ -0,0 +1,55 @@ +package uk.gov.hmcts.opal.authentication.exception; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.PrintWriter; + +@Component +public class CustomAuthenticationExceptions implements AuthenticationEntryPoint, AccessDeniedHandler { + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + + // Set the response status code + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + // Set the content type of the response + response.setContentType("application/json"); + + // Write the custom message to the response body + try (PrintWriter writer = response.getWriter()) { + writer.write("{\"error\": \"Unauthorized\", \"message\":" + + " \"Unauthorized: request could not be authorized\"}"); + } + } + + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + + // Set the response status code + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + + // Set the content type of the response + response.setContentType("application/json"); + + // Write the custom message to the response body + try (PrintWriter writer = response.getWriter()) { + writer.write("{\"error\": \"Forbidden\", \"message\": " + + "\"Forbidden: access is forbidden for this user\"}"); + } + } +} + diff --git a/src/main/java/uk/gov/hmcts/opal/controllers/ExceptionControllerAdvice.java b/src/main/java/uk/gov/hmcts/opal/controllers/ExceptionControllerAdvice.java deleted file mode 100644 index 437c9590..00000000 --- a/src/main/java/uk/gov/hmcts/opal/controllers/ExceptionControllerAdvice.java +++ /dev/null @@ -1,66 +0,0 @@ -package uk.gov.hmcts.opal.controllers; - -import lombok.extern.slf4j.Slf4j; -import org.hibernate.PropertyValueException; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; - -import java.util.Map; - -@ControllerAdvice -@Slf4j(topic = "ExceptionControllerAdvice") -public class ExceptionControllerAdvice { - - public static final String ERROR_MESSAGE = "errorMessage"; - - @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); - } - - @ExceptionHandler - public ResponseEntity> handleHttpMessageNotReadableException( - HttpMessageNotReadableException hmnre) { - - log.error(":handleHttpMessageNotReadableException: {}", hmnre.getMessage()); - Map body = Map.of( - ERROR_MESSAGE, hmnre.getMessage() - ); - return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).body(body); - } - - @ExceptionHandler - public ResponseEntity> handleInvalidDataAccessApiUsageException( - 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); - } - - @ExceptionHandler - public ResponseEntity> handleInvalidDataAccessResourceUsageException( - InvalidDataAccessResourceUsageException idarue) { - - 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); - } -} 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 274f8323..f58a67fb 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 @@ -1,18 +1,33 @@ package uk.gov.hmcts.opal.controllers.advice; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceException; +import jakarta.persistence.QueryTimeoutException; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.PropertyValueException; +import org.postgresql.util.PSQLException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.http.HttpStatus; 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.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import uk.gov.hmcts.opal.authentication.exception.MissingRequestHeaderException; import uk.gov.hmcts.opal.authentication.service.AccessTokenService; import uk.gov.hmcts.opal.authorisation.aspect.PermissionNotAllowedException; +import uk.gov.hmcts.opal.exception.OpalApiException; import uk.gov.hmcts.opal.launchdarkly.FeatureDisabledException; +import java.util.Map; + import static uk.gov.hmcts.opal.authentication.service.AccessTokenService.AUTH_HEADER; import static uk.gov.hmcts.opal.util.HttpUtil.extractPreferredUsername; @@ -21,6 +36,8 @@ @RequiredArgsConstructor public class GlobalExceptionHandler { + public static final String ERROR_MESSAGE = "errorMessage"; + private final AccessTokenService tokenService; @ExceptionHandler(FeatureDisabledException.class) @@ -39,7 +56,149 @@ public ResponseEntity handlePermissionNotAllowedException(Exception ex, String authorization = request.getHeader(AUTH_HEADER); String preferredName = extractPreferredUsername(authorization, tokenService); String message = String.format("For user %s, %s", preferredName, ex.getMessage()); - log.warn(message); + log.error(message); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(message); } + + @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) + public ResponseEntity> handleHttpMediaTypeNotAcceptableException( + HttpMediaTypeNotAcceptableException ex) { + + 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); + } + + @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); + } + + @ExceptionHandler + public ResponseEntity> handleHttpMessageNotReadableException( + HttpMessageNotReadableException hmnre) { + + log.error(":handleHttpMessageNotReadableException: {}", hmnre.getMessage()); + Map body = Map.of( + ERROR_MESSAGE, hmnre.getMessage() + ); + return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).body(body); + } + + @ExceptionHandler + public ResponseEntity> handleInvalidDataAccessApiUsageException( + 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); + } + + @ExceptionHandler + public ResponseEntity> handleInvalidDataAccessResourceUsageException( + InvalidDataAccessResourceUsageException idarue) { + + 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); + } + + @ExceptionHandler + public ResponseEntity> handleEntityNotFoundException( + EntityNotFoundException entityNotFoundException) { + + 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); + } + + @ExceptionHandler(OpalApiException.class) + public ResponseEntity> handleOpalApiException( + OpalApiException opalApiException) { + + 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); + } + + @ExceptionHandler({ServletException.class, TransactionSystemException.class, PersistenceException.class}) + 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); + } + + // 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); + } + + @ExceptionHandler + public ResponseEntity> handlePsqlException( + PSQLException psqlException) { + + log.error(":handlePSQLException: {}", psqlException.getMessage()); + 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 = Map.of( + "error", "Internal Server Error", "message", psqlException.getMessage() + ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body); + } + + @ExceptionHandler + public ResponseEntity> handleDataAccessResourceFailureException( + DataAccessResourceFailureException dataAccessResourceFailureException) { + + 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); + } } diff --git a/src/main/java/uk/gov/hmcts/opal/controllers/exception/DataLookUpError.java b/src/main/java/uk/gov/hmcts/opal/controllers/exception/DataLookUpError.java new file mode 100644 index 00000000..923368c4 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/controllers/exception/DataLookUpError.java @@ -0,0 +1,27 @@ +package uk.gov.hmcts.opal.controllers.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import uk.gov.hmcts.opal.exception.OpalApiError; + +@Getter +@RequiredArgsConstructor +public enum DataLookUpError implements OpalApiError { + + NO_CONTENT_FOUND_AT_URI("100", + HttpStatus.NOT_FOUND, + "The Resource identified by the provided URI cannot be found"); + + private static final String ERROR_TYPE_PREFIX = "DATA_LOOK_UP"; + + private final String errorTypeNumeric; + private final HttpStatus httpStatus; + private final String title; + + @Override + public String getErrorTypePrefix() { + return ERROR_TYPE_PREFIX; + } + +} diff --git a/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java b/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java index 79b0181b..bc299c70 100644 --- a/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java +++ b/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java @@ -74,7 +74,7 @@ public class DraftAccountEntity { @Column(name = "account_status", length = 30, nullable = false) private String accountStatus; - @Column(name = "status_reason", columnDefinition = "json") + @Column(name = "timeline_data", columnDefinition = "json") @JsonDeserialize(using = KeepAsJsonDeserializer.class) @JsonRawValue private String timelineData; diff --git a/src/main/java/uk/gov/hmcts/opal/util/HttpUtil.java b/src/main/java/uk/gov/hmcts/opal/util/HttpUtil.java index dbc37cf9..50abe277 100644 --- a/src/main/java/uk/gov/hmcts/opal/util/HttpUtil.java +++ b/src/main/java/uk/gov/hmcts/opal/util/HttpUtil.java @@ -2,24 +2,36 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import uk.gov.hmcts.opal.authentication.service.AccessTokenService; import java.util.List; import java.util.Optional; +@SuppressWarnings("unchecked") public class HttpUtil { - public static ResponseEntity> buildResponse(List contents) { - if (contents == null || contents.isEmpty()) { - return ResponseEntity.noContent().build(); - } + private static final String NOT_FOUND_MESSAGE = + """ + { "error": "Not Found", "message": "No resource found at provided URI"}"""; + + private static final MultiValueMap HEADERS; + static { + HEADERS = new LinkedMultiValueMap<>(); + HEADERS.add("content-type", "application/json"); + } + + public static ResponseEntity> buildResponse(List contents) { + //return list even if empty return ResponseEntity.ok(contents); } public static ResponseEntity buildResponse(T contents) { if (contents == null) { - return ResponseEntity.noContent().build(); + contents = (T) NOT_FOUND_MESSAGE; + return new ResponseEntity<>(contents, HEADERS, HttpStatus.NOT_FOUND); } return ResponseEntity.ok(contents); @@ -27,7 +39,8 @@ public static ResponseEntity buildResponse(T contents) { public static ResponseEntity buildCreatedResponse(T contents) { if (contents == null) { - return ResponseEntity.noContent().build(); + contents = (T) NOT_FOUND_MESSAGE; + return new ResponseEntity<>(contents, HEADERS, HttpStatus.NOT_FOUND); } return new ResponseEntity<>(contents, HttpStatus.CREATED); 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 new file mode 100644 index 00000000..cf1d9c5e --- /dev/null +++ b/src/test/java/uk/gov/hmcts/opal/authentication/exception/CustomAuthenticationExceptionsTest.java @@ -0,0 +1,57 @@ +package uk.gov.hmcts.opal.authentication.exception; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; + +import java.io.IOException; +import java.io.PrintWriter; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CustomAuthenticationExceptionsTest { + + private CustomAuthenticationExceptions customAuthenticationExceptions; + private HttpServletRequest request; + private HttpServletResponse response; + private PrintWriter writer; + + @BeforeEach + void setUp() throws IOException { + customAuthenticationExceptions = new CustomAuthenticationExceptions(); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + writer = mock(PrintWriter.class); + when(response.getWriter()).thenReturn(writer); + } + + @Test + void commenceShouldReturnUnauthorizedResponse() throws IOException, ServletException { + AuthenticationException authException = mock(AuthenticationException.class); + + customAuthenticationExceptions.commence(request, response, authException); + + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json"); + verify(writer).write("{\"error\": \"Unauthorized\", \"message\": " + + "\"Unauthorized: request could not be authorized\"}"); + } + + @Test + void handleShouldReturnForbiddenResponse() throws IOException, ServletException { + AccessDeniedException accessDeniedException = mock(AccessDeniedException.class); + + customAuthenticationExceptions.handle(request, response, accessDeniedException); + + verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN); + verify(response).setContentType("application/json"); + verify(writer).write("{\"error\": \"Forbidden\", \"message\": " + + "\"Forbidden: access is forbidden for this user\"}"); + } +} diff --git a/src/test/java/uk/gov/hmcts/opal/controllers/DefendantAccountControllerTest.java b/src/test/java/uk/gov/hmcts/opal/controllers/DefendantAccountControllerTest.java index 7f022425..ba852d04 100644 --- a/src/test/java/uk/gov/hmcts/opal/controllers/DefendantAccountControllerTest.java +++ b/src/test/java/uk/gov/hmcts/opal/controllers/DefendantAccountControllerTest.java @@ -33,6 +33,9 @@ class DefendantAccountControllerTest { static final String BEARER_TOKEN = "Bearer a_token_here"; + static final String NOT_FOUND_JSON = """ + { "error": "Not Found", "message": "No resource found at provided URI"}"""; + @Mock private DefendantAccountService defendantAccountService; @@ -65,7 +68,7 @@ public void testGetDefendantAccount_Success() { @Test public void testGetDefendantAccount_NoContent() { - + // Arrange when(defendantAccountService.getDefendantAccount(any(AccountEnquiryDto.class))).thenReturn(null); // Act @@ -73,11 +76,12 @@ public void testGetDefendantAccount_NoContent() { (short) 1, "", BEARER_TOKEN); // Assert - assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode()); + assertEquals(HttpStatus.NOT_FOUND, responseEntity.getStatusCode()); verify(defendantAccountService, times(1)).getDefendantAccount(any( AccountEnquiryDto.class)); } + @Test public void testPutDefendantAccount_Success() { // Arrange @@ -157,6 +161,8 @@ public void testAddNote_Success() { @Test public void testAddNote_NoContent() { + // Arrange + when(noteService.saveNote(any(NoteDto.class))).thenReturn(null); when(userStateService.getUserStateUsingAuthToken(any())).thenReturn(createUserState()); @@ -165,9 +171,10 @@ public void testAddNote_NoContent() { ResponseEntity responseEntity = defendantAccountController.addNote(addNote, BEARER_TOKEN); // Assert - assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode()); + assertEquals(HttpStatus.NOT_FOUND, responseEntity.getStatusCode()); verify(noteService, times(1)).saveNote(any( NoteDto.class)); + } @Test @@ -193,6 +200,9 @@ public void testNotes_Success() { @Test public void testNotes_NoContent() { + // Arrange + NoteDto mockNote = new NoteDto(); + when(noteService.searchNotes(any(NoteSearchDto.class))).thenReturn(null); // Act @@ -201,9 +211,11 @@ public void testNotes_NoContent() { .getNotesForDefendantAccount("1", BEARER_TOKEN); // Assert - assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode()); + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(null, responseEntity.getBody()); verify(noteService, times(1)).searchNotes(any( NoteSearchDto.class)); + } } diff --git a/src/test/java/uk/gov/hmcts/opal/controllers/ExceptionControllerAdviceTest.java b/src/test/java/uk/gov/hmcts/opal/controllers/ExceptionControllerAdviceTest.java deleted file mode 100644 index 3b9a8d71..00000000 --- a/src/test/java/uk/gov/hmcts/opal/controllers/ExceptionControllerAdviceTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package uk.gov.hmcts.opal.controllers; - -import org.hibernate.PropertyValueException; -import org.htmlunit.http.HttpStatus; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@ExtendWith(MockitoExtension.class) -class ExceptionControllerAdviceTest { - - @InjectMocks - private ExceptionControllerAdvice exceptionControllerAdvice; - - @Test - void handlePropertyValueException() { - // Arrange - PropertyValueException pve = new PropertyValueException("A Test Message", "DraftAccountEntity", "account"); - // Act - ResponseEntity> response = exceptionControllerAdvice.handlePropertyValueException(pve); - // Assert - assertEquals(HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); - } - - @Test - void handleHttpMessageNotReadableException() { - // Arrange - HttpInputMessage input = Mockito.mock(HttpInputMessage.class); - HttpMessageNotReadableException hmnre = new HttpMessageNotReadableException("A Test Message", input); - // Act - ResponseEntity> response = exceptionControllerAdvice - .handleHttpMessageNotReadableException(hmnre); - // Assert - assertEquals(HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); - } - - @Test - void handleInvalidDataAccessApiUsageException() { - // Arrange - InvalidDataAccessApiUsageException idaaue = new InvalidDataAccessApiUsageException("A Test Message"); - // Act - ResponseEntity> response = exceptionControllerAdvice - .handleInvalidDataAccessApiUsageException(idaaue); - // Assert - assertEquals(HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); - } - - @Test - void handleInvalidDataAccessResourceUsageException() { - // Arrange - InvalidDataAccessResourceUsageException idarue = new InvalidDataAccessResourceUsageException("A Test Message"); - // Act - ResponseEntity> response = exceptionControllerAdvice - .handleInvalidDataAccessResourceUsageException(idarue); - // Assert - assertEquals(HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); - } - -} 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 779b4771..8ef6f708 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,33 @@ package uk.gov.hmcts.opal.controllers.advice; +import jakarta.persistence.EntityNotFoundException; import jakarta.servlet.http.HttpServletRequest; +import org.hibernate.PropertyValueException; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; 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.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; import uk.gov.hmcts.opal.authentication.exception.MissingRequestHeaderException; import uk.gov.hmcts.opal.authentication.service.AccessTokenService; import uk.gov.hmcts.opal.authorisation.aspect.PermissionNotAllowedException; import uk.gov.hmcts.opal.authorisation.model.Permissions; +import uk.gov.hmcts.opal.exception.OpalApiException; import uk.gov.hmcts.opal.launchdarkly.FeatureDisabledException; +import java.util.Map; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @@ -91,4 +103,73 @@ void handleAccessDeniedException_ShouldReturnForbiddenResponse() { // Assert assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); } + + @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()); + } + + @Test + void handleHttpMessageNotReadableException() { + // Arrange + HttpInputMessage input = Mockito.mock(HttpInputMessage.class); + HttpMessageNotReadableException hmnre = new HttpMessageNotReadableException("A Test Message", input); + // Act + ResponseEntity> response = globalExceptionHandler + .handleHttpMessageNotReadableException(hmnre); + // Assert + assertEquals(org.htmlunit.http.HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); + } + + @Test + void handleInvalidDataAccessApiUsageException() { + // Arrange + InvalidDataAccessApiUsageException idaaue = new InvalidDataAccessApiUsageException("A Test Message"); + // Act + ResponseEntity> response = globalExceptionHandler + .handleInvalidDataAccessApiUsageException(idaaue); + // Assert + assertEquals(org.htmlunit.http.HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); + } + + @Test + void handleInvalidDataAccessResourceUsageException() { + // Arrange + InvalidDataAccessResourceUsageException idarue = new InvalidDataAccessResourceUsageException("A Test Message"); + // Act + ResponseEntity> response = globalExceptionHandler + .handleInvalidDataAccessResourceUsageException(idarue); + // Assert + assertEquals(org.htmlunit.http.HttpStatus.IM_A_TEAPOT_418, response.getStatusCode().value()); + } + + @Test + void handleEntityNotFoundException_ReturnsNotFound() { + EntityNotFoundException ex = new EntityNotFoundException("Entity not found"); + ResponseEntity> response = globalExceptionHandler.handleEntityNotFoundException(ex); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + assertEquals("Entity not found", response.getBody().get(GlobalExceptionHandler.ERROR_MESSAGE)); + } + + @Test + void handleOpalApiException_ReturnsInternalServerError() { + OpalApiException ex = new OpalApiException(AuthenticationError.FAILED_TO_PARSE_ACCESS_TOKEN, + "Internal Server Error"); + 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")); + } + + @Test + void handleHttpMediaTypeNotAcceptableException_ReturnsNotAcceptable() { + HttpMediaTypeNotAcceptableException ex = new HttpMediaTypeNotAcceptableException("Not acceptable"); + ResponseEntity> response = globalExceptionHandler + .handleHttpMediaTypeNotAcceptableException(ex); + assertEquals(HttpStatus.NOT_ACCEPTABLE, response.getStatusCode()); + } } diff --git a/src/test/java/uk/gov/hmcts/opal/controllers/develop/NoteControllerTest.java b/src/test/java/uk/gov/hmcts/opal/controllers/develop/NoteControllerTest.java index 808b47fa..ad594f16 100644 --- a/src/test/java/uk/gov/hmcts/opal/controllers/develop/NoteControllerTest.java +++ b/src/test/java/uk/gov/hmcts/opal/controllers/develop/NoteControllerTest.java @@ -16,6 +16,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -83,7 +84,8 @@ public void testFindNoteByAssociated_NoContent() { ResponseEntity> responseEntity = noteController.getNotesByAssociatedRecord("type", "1"); // Assert - assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode()); + assertEquals(responseEntity.getStatusCode(), HttpStatus.OK); + assertNull(responseEntity.getBody()); verify(noteService, times(1)).searchNotes(any( NoteSearchDto.class)); } @@ -118,7 +120,8 @@ public void testNotesSearch_NoContent() { ResponseEntity> responseEntity = noteController.postNotesSearch(criteria); // Assert - assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode()); + assertEquals(responseEntity.getStatusCode(), HttpStatus.OK); + assertNull(responseEntity.getBody()); verify(noteService, times(1)).searchNotes(any( NoteSearchDto.class)); } diff --git a/src/test/java/uk/gov/hmcts/opal/util/ResponseUtilTest.java b/src/test/java/uk/gov/hmcts/opal/util/ResponseUtilTest.java index e156e909..c6b8015c 100644 --- a/src/test/java/uk/gov/hmcts/opal/util/ResponseUtilTest.java +++ b/src/test/java/uk/gov/hmcts/opal/util/ResponseUtilTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class ResponseUtilTest { @@ -26,7 +27,7 @@ void buildResponse_withNonNullList_returnsOkResponse() { } @Test - void buildResponse_withEmptyList_returnsNoContentResponse() { + void buildResponse_withEmptyList_returnsOkResponse() { // Arrange List responseList = Collections.emptyList(); @@ -34,12 +35,12 @@ void buildResponse_withEmptyList_returnsNoContentResponse() { ResponseEntity> responseEntity = HttpUtil.buildResponse(responseList); // Assert - assertEquals(204, responseEntity.getStatusCode().value()); - assertNull(responseEntity.getBody()); + assertEquals(200, responseEntity.getStatusCode().value()); + assertTrue(responseEntity.getBody().isEmpty()); } @Test - void buildResponse_withNullList_returnsNoContentResponse() { + void buildResponse_withNullList_returnsOkResponse() { // Arrange List responseList = null; @@ -47,7 +48,7 @@ void buildResponse_withNullList_returnsNoContentResponse() { ResponseEntity> responseEntity = HttpUtil.buildResponse(responseList); // Assert - assertEquals(204, responseEntity.getStatusCode().value()); + assertEquals(200, responseEntity.getStatusCode().value()); assertNull(responseEntity.getBody()); } @@ -66,7 +67,7 @@ void buildResponse_withNonNullString_returnsOkResponse() { @Test - void buildResponse_withNullString_returnsNoContentResponse() { + void buildResponse_withNullString_throwsOpalApiException() { // Arrange String response = null; @@ -74,7 +75,9 @@ void buildResponse_withNullString_returnsNoContentResponse() { ResponseEntity responseEntity = HttpUtil.buildResponse(response); // Assert - assertEquals(204, responseEntity.getStatusCode().value()); - assertNull(responseEntity.getBody()); + assertEquals(404, responseEntity.getStatusCode().value()); + assertEquals(""" + { "error": "Not Found", "message": "No resource found at provided URI"}""", + responseEntity.getBody()); } }