Skip to content

Commit

Permalink
PO-690: Add Exception Path handling for GET draft account endpoint (#594
Browse files Browse the repository at this point in the history
)
  • Loading branch information
RustyHMCTS authored Oct 17, 2024
1 parent 2b7e8bc commit cfdd6b6
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.QueryTimeoutException;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.stubbing.OngoingStubbing;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -18,6 +18,7 @@
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import uk.gov.hmcts.opal.authentication.service.AccessTokenService;
import uk.gov.hmcts.opal.authorisation.model.Permissions;
import uk.gov.hmcts.opal.authorisation.model.UserState;
Expand All @@ -44,7 +45,6 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
Expand Down Expand Up @@ -89,7 +89,7 @@ class DraftAccountControllerIntegrationTest {
private JsonSchemaValidationService jsonSchemaValidationService;

@Test
void testGetDraftAccountById() throws Exception {
void testGetDraftAccountById_success() throws Exception {
DraftAccountEntity draftAccountEntity = createDraftAccountEntity();

when(draftAccountService.getDraftAccount(1L)).thenReturn(draftAccountEntity);
Expand All @@ -112,15 +112,32 @@ void testGetDraftAccountById() throws Exception {
assertTrue(jsonSchemaValidationService.isValid(body, GET_DRAFT_ACCOUNT_RESPONSE));
}


@Test
void testGetDraftAccountById_WhenDraftAccountDoesNotExist() throws Exception {
void testGetDraftAccountById_notFound_404Response() throws Exception {
when(draftAccountService.getDraftAccount(2L)).thenReturn(null);

mockMvc.perform(get(URL_BASE + "/2").header("authorization", "Bearer some_value"))
.andExpect(status().isNotFound());
}

@Test
void testGetDraftAccountById_trap406Response() throws Exception {
when(draftAccountService.getDraftAccount(1L)).thenReturn(createDraftAccountEntity());
shouldReturn406WhenResponseContentTypeNotSupported(get(URL_BASE + "/1"));
}

@Test
void testGetDraftAccountById_trap408Response() throws Exception {
shouldReturn408WhenTimeout(get(URL_BASE + "/1"),
when(draftAccountService.getDraftAccount(1L)));
}

@Test
void testGetDraftAccountById_trap503Response() throws Exception {
shouldReturn503WhenDownstreamServiceIsUnavailable(get(URL_BASE + "/1"),
when(draftAccountService.getDraftAccount(1L)));
}

@Test
void testGetDraftAccountsSummaries_noParams() throws Exception {
DraftAccountEntity draftAccountEntity = createDraftAccountEntity();
Expand Down Expand Up @@ -179,6 +196,22 @@ void testGetDraftAccountsSummaries_noPermission() throws Exception {
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}

@Test
void testGetDraftAccountsSummaries_trap406Response() throws Exception {
shouldReturn406WhenResponseContentTypeNotSupported(get(URL_BASE));
}

@Test
void testGetDraftAccountsSummaries_trap408Response() throws Exception {
shouldReturn408WhenTimeout(get(URL_BASE), when(draftAccountService.getDraftAccounts(any(), any(), any())));
}

@Test
void testGetDraftAccountsSummaries_trap503Response() throws Exception {
shouldReturn503WhenDownstreamServiceIsUnavailable(get(URL_BASE),
when(draftAccountService.getDraftAccounts(any(), any(), any())));
}

private String checkStandardSummaryExpectations(ResultActions actions) throws Exception {
return actions.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
Expand Down Expand Up @@ -220,32 +253,6 @@ void testSearchDraftAccountsPost_WhenDraftAccountDoesNotExist() throws Exception
.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(URL_BASE + "/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(URL_BASE + "/1")
.header("Authorization", "Bearer " + "some_value")
.accept("application/xml"))
.andExpect(status().isNotAcceptable());
}

private DraftAccountEntity createDraftAccountEntity() {
return DraftAccountEntity.builder()
Expand All @@ -261,26 +268,6 @@ private DraftAccountEntity createDraftAccountEntity() {
.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(URL_BASE + "/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"
}"""));
}

@Test
void testDeleteDraftAccountById() throws Exception {
Expand Down Expand Up @@ -478,18 +465,13 @@ void testPostDraftAccount_JsonSchemaValidationException() throws Exception {
@Test
void testPostDraftAccount_permission() throws Exception {

String validRequestBody = validCreateRequestBody();
AddDraftAccountRequestDto dto = ToJsonString.toClassInstance(validRequestBody, AddDraftAccountRequestDto.class);
LocalDateTime created = LocalDateTime.now();
DraftAccountEntity entity = toEntity(dto, created);

String validRequestBody = setupValidPostRequest();
when(userStateService.checkForAuthorisedUser(any())).thenReturn(allPermissionsUser());
when(draftAccountService.submitDraftAccount(any())).thenReturn(entity);

MvcResult result = mockMvc.perform(post(URL_BASE)
.header("authorization", "Bearer some_value")
.contentType(MediaType.APPLICATION_JSON)
.content(validCreateRequestBody()))
.content(validRequestBody))
.andExpect(status().isCreated())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.submitted_by").value("BUUID1"))
Expand All @@ -509,18 +491,13 @@ void testPostDraftAccount_permission() throws Exception {
@Test
void testPostDraftAccount_noPermission() throws Exception {

String validRequestBody = validCreateRequestBody();
AddDraftAccountRequestDto dto = ToJsonString.toClassInstance(validRequestBody, AddDraftAccountRequestDto.class);
LocalDateTime created = LocalDateTime.now();
DraftAccountEntity entity = toEntity(dto, created);

String validRequestBody = setupValidPostRequest();
when(userStateService.checkForAuthorisedUser(any())).thenReturn(noPermissionsUser());
when(draftAccountService.submitDraftAccount(any())).thenReturn(entity);

MvcResult result = mockMvc.perform(post(URL_BASE)
.header("authorization", "Bearer some_value")
.contentType(MediaType.APPLICATION_JSON)
.content(validCreateRequestBody()))
.content(validRequestBody))
.andExpect(status().isForbidden())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.error").value("Forbidden"))
Expand All @@ -536,19 +513,15 @@ void testPostDraftAccount_noPermission() throws Exception {
@Test
void testPostDraftAccount_wrongPermission() throws Exception {

String validRequestBody = validCreateRequestBody();
AddDraftAccountRequestDto dto = ToJsonString.toClassInstance(validRequestBody, AddDraftAccountRequestDto.class);
LocalDateTime created = LocalDateTime.now();
DraftAccountEntity entity = toEntity(dto, created);
String validRequestBody = setupValidPostRequest();

when(userStateService.checkForAuthorisedUser(any())).thenReturn(
permissionUser((short)5, Permissions.CHECK_VALIDATE_DRAFT_ACCOUNTS, Permissions.ACCOUNT_ENQUIRY));
when(draftAccountService.submitDraftAccount(any())).thenReturn(entity);

MvcResult result = mockMvc.perform(post(URL_BASE)
.header("authorization", "Bearer some_value")
.contentType(MediaType.APPLICATION_JSON)
.content(validCreateRequestBody()))
.content(validRequestBody))
.andExpect(status().isForbidden())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.error").value("Forbidden"))
Expand All @@ -561,6 +534,86 @@ void testPostDraftAccount_wrongPermission() throws Exception {
logger.info(":testPostDraftAccount_permission: Response body:\n" + ToJsonString.toPrettyJson(body));
}

@Test
void testPostDraftAccount_trap406Response() throws Exception {
String validRequestBody = setupValidPostRequest();
shouldReturn406WhenResponseContentTypeNotSupported(
post(URL_BASE).contentType(MediaType.APPLICATION_JSON).content(validRequestBody));
}

@Test
void testPostDraftAccount_trap408Response() throws Exception {
String validRequestBody = setupValidPostRequest();
shouldReturn408WhenTimeout(
post(URL_BASE).contentType(MediaType.APPLICATION_JSON).content(validRequestBody),
when(draftAccountService.submitDraftAccount(any())));
}

@Test
void testPostDraftAccount_trap503Response() throws Exception {
String validRequestBody = setupValidPostRequest();
shouldReturn503WhenDownstreamServiceIsUnavailable(
post(URL_BASE).contentType(MediaType.APPLICATION_JSON).content(validRequestBody),
when(draftAccountService.submitDraftAccount(any())));
}

private String setupValidPostRequest() {
String validRequestBody = validCreateRequestBody();
AddDraftAccountRequestDto dto = ToJsonString.toClassInstance(validRequestBody, AddDraftAccountRequestDto.class);
LocalDateTime created = LocalDateTime.now();
DraftAccountEntity entity = toEntity(dto, created);
when(draftAccountService.submitDraftAccount(any())).thenReturn(entity);
return validRequestBody;
}

void shouldReturn406WhenResponseContentTypeNotSupported(MockHttpServletRequestBuilder reqBuilder) throws Exception {
when(userStateService.checkForAuthorisedUser(any())).thenReturn(allPermissionsUser());
mockMvc.perform(reqBuilder
.header("Authorization", "Bearer " + "some_value")
.accept("application/xml"))
.andExpect(status().isNotAcceptable());
}


void shouldReturn408WhenTimeout(MockHttpServletRequestBuilder reqBuilder, OngoingStubbing<?> stubbing)
throws Exception {
// Simulating a timeout exception when the service is called
stubbing.thenThrow(new QueryTimeoutException());

when(userStateService.checkForAuthorisedUser(any())).thenReturn(allPermissionsUser());

mockMvc.perform(reqBuilder
.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"
}"""));
}


void shouldReturn503WhenDownstreamServiceIsUnavailable(MockHttpServletRequestBuilder reqBuilder,
OngoingStubbing<?> stubbing) throws Exception {
stubbing.thenAnswer(
invocation -> {
throw new PSQLException("Connection refused", PSQLState.CONNECTION_FAILURE, new ConnectException());
});

when(userStateService.checkForAuthorisedUser(any())).thenReturn(allPermissionsUser());

mockMvc.perform(reqBuilder
.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"
}"""));
}

private String validCreateRequestBody() {
return """
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ public ResponseEntity<DraftAccountResponseDto> getDraftAccountById(

log.info(":GET:getDraftAccountById: draftAccountId: {}", draftAccountId);

userStateService.checkForAuthorisedUser(authHeaderValue);

UserState userState = userStateService.checkForAuthorisedUser(authHeaderValue);
DraftAccountEntity response = draftAccountService.getDraftAccount(draftAccountId);

return buildResponse(Optional.ofNullable(response).map(this::toGetResponseDto).orElse(null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ public ResponseEntity<Map<String, String>> handleHttpMediaTypeNotAcceptableExcep
public ResponseEntity<Map<String, String>> handleMethodArgumentTypeMismatchException(
MethodArgumentTypeMismatchException ex) {

log.error(":handleHttpMediaTypeNotAcceptableException: {}", ex.getMessage());
log.error(":handleHttpMediaTypeNotAcceptableException:", ex.getCause());
log.error(":handleMethodArgumentTypeMismatchException: {}", ex.getMessage());
log.error(":handleMethodArgumentTypeMismatchException:", ex.getCause());

Map<String, String> body = new LinkedHashMap<>();
body.put(ERROR, "Not Acceptable");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonInclude(JsonInclude.Include.NON_NULL) // This line forces the HTTP Response to be of type 'application/json'
public class DraftAccountResponseDto implements ToJsonString {

@JsonProperty("draft_account_id")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package uk.gov.hmcts.opal.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -12,6 +13,7 @@
@Getter
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL) // This line forces the HTTP Response to be of type 'application/json'
public class DraftAccountsResponseDto {
private Integer count;
private List<DraftAccountSummaryDto> summaries;
Expand Down

0 comments on commit cfdd6b6

Please sign in to comment.