Skip to content

Commit

Permalink
Refactored (#62)
Browse files Browse the repository at this point in the history
Co-authored-by: Ewan Donovan <ewan.donovan@digital.justice.com>
  • Loading branch information
Ewan-Donovan and Ewan Donovan authored Jan 31, 2025
1 parent 83804cc commit 451351e
Show file tree
Hide file tree
Showing 6 changed files with 487 additions and 185 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,33 @@ import com.google.gson.GsonBuilder
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import uk.gov.justice.digital.hmpps.learnerrecordsapi.integration.IntegrationTestBase
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.gsonadapters.LocalDateAdapter
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.gsonadapters.ResponseTypeAdapter
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.Gender
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.LRSResponseType
import java.time.LocalDate

// Tests that when exceptions are thrown, the exception handler will pick them up and behave correctly.
// Test endpoints that throw exceptions are found in TestExceptionResource in this same package.

class HmppsBoldLrsExceptionHandlerTest : IntegrationTestBase() {

val gson = GsonBuilder()
.registerTypeAdapter(LocalDate::class.java, LocalDateAdapter().nullSafe())
.registerTypeAdapter(LRSResponseType::class.java, ResponseTypeAdapter().nullSafe())
.disableHtmlEscaping()
.create()

@Test
fun `should return validation errors when user postcode is invalid`() {
val expectedResponse = HmppsBoldLrsExceptionHandler.ErrorResponse(
HttpStatus.BAD_REQUEST,
"Validation Failed",
"Please correct the error and retry",
"Validation(s) failed for [lastKnownPostCode]",
"Validation(s) failed for [lastKnownPostCode] with reason(s): [must match \"^[A-Z]{1,2}[0-9R][0-9A-Z]? ?[0-9][ABDEFGHJLNPQRSTUWXYZ]{2}|BFPO ?[0-9]{1,4}|([AC-FHKNPRTV-Y]\\d{2}|D6W)? ?[0-9AC-FHKNPRTV-Y]{4}\$\"]",
)

val findLearnerByDemographicsRequest =
uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.LearnersRequest(
"Darcie",
"Tucker",
LocalDate.parse("2024-01-01"),
Gender.MALE,
"ABC123",
)

private fun testExceptionHandling(
uri: String,
expectedResponse: HmppsBoldLrsExceptionHandler.ErrorResponse,
expectedStatus: HttpStatus,
) {
val actualResponse = webTestClient.post()
.uri("/learners")
.headers(setAuthorisation(roles = listOf("ROLE_LEARNER_RECORDS_SEARCH__RO")))
.bodyValue(findLearnerByDemographicsRequest)
.accept(MediaType.parseMediaType("application/json"))
.uri(uri)
.headers(setAuthorisation(roles = listOf("ROLE_TEMPLATE_KOTLIN__UI")))
.exchange()
.expectStatus()
.isBadRequest
.isEqualTo(expectedStatus)
.expectBody()
.returnResult()
.responseBody
Expand All @@ -56,179 +40,80 @@ class HmppsBoldLrsExceptionHandlerTest : IntegrationTestBase() {
}

@Test
fun `should return validation errors when user givenName is invalid`() {
fun `should catch validation exceptions (MethodArgumentNotValidException) and return BadRequest`() {
val expectedResponse = HmppsBoldLrsExceptionHandler.ErrorResponse(
HttpStatus.BAD_REQUEST,
"Validation Failed",
"Please correct the error and retry",
"Validation(s) failed for [givenName]",
"Validation(s) failed for [givenName] with reason(s): [must match \"^[A-Za-z' ,.-]{3,35}$\"]",
"Validation(s) failed for [testField]",
"Validation(s) failed for [testField] with reason(s): [Validation failed]",
)

val findLearnerByDemographicsRequest =
uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.LearnersRequest(
"DarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcieDarcie",
"Tucker",
LocalDate.parse("2024-01-01"),
Gender.MALE,
"CV49EE",
)

val actualResponse = webTestClient.post()
.uri("/learners")
.headers(setAuthorisation(roles = listOf("ROLE_LEARNER_RECORDS_SEARCH__RO")))
.bodyValue(findLearnerByDemographicsRequest)
.accept(MediaType.parseMediaType("application/json"))
.exchange()
.expectStatus()
.isBadRequest
.expectBody()
.returnResult()
.responseBody

val actualResponseString = actualResponse?.toString(Charsets.UTF_8)
assertThat(actualResponseString).isEqualTo(gson.toJson(expectedResponse))
testExceptionHandling("/test/validation", expectedResponse, expectedStatus = HttpStatus.BAD_REQUEST)
}

@Test
fun `should return validation errors when user familyName is invalid`() {
fun `should catch No Resource exceptions (NoResourceFoundException) and return Not Found`() {
val expectedResponse = HmppsBoldLrsExceptionHandler.ErrorResponse(
HttpStatus.BAD_REQUEST,
"Validation Failed",
"Please correct the error and retry",
"Validation(s) failed for [familyName]",
"Validation(s) failed for [familyName] with reason(s): [must match \"^[A-Za-z' ,.-]{3,35}\$\"]",
HttpStatus.NOT_FOUND,
"No Resource Found",
"No resource found failure: No static resource someUnknownEndpoint.",
"Requested Resource not found on the server",
moreInfo = "Requested Resource not found on the server",
)
val findLearnerByDemographicsRequest =
uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.LearnersRequest(
"Darcie",
"TuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTuckerTucker",
LocalDate.parse("2024-01-01"),
Gender.MALE,
"CV49EE",
)

val actualResponse = webTestClient.post()
.uri("/learners")
.headers(setAuthorisation(roles = listOf("ROLE_LEARNER_RECORDS_SEARCH__RO")))
.bodyValue(findLearnerByDemographicsRequest)
.accept(MediaType.parseMediaType("application/json"))
.exchange()
.expectStatus()
.isBadRequest
.expectBody()
.returnResult()
.responseBody

val actualResponseString = actualResponse?.toString(Charsets.UTF_8)
assertThat(actualResponseString).isEqualTo(gson.toJson(expectedResponse))
testExceptionHandling("/someUnknownEndpoint", expectedResponse, expectedStatus = HttpStatus.NOT_FOUND)
}

@Test
fun `should return validation errors when user gender is invalid`() {
fun `should catch missing mandatory field exceptions (HttpMessageNotReadableException) and return BadRequest`() {
val expectedResponse = HmppsBoldLrsExceptionHandler.ErrorResponse(
HttpStatus.BAD_REQUEST,
"Unreadable HTTP message",
"Unreadable HTTP message",
"JSON parse error: Cannot deserialize value of type `uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.Gender` from String \"TESTINGENUM\": not one of the values accepted for Enum class: [NOT_SPECIFIED, MALE, NOT_KNOWN, FEMALE]",
"JSON parse error: Missing Field",
"Unreadable HTTP message",
)

val findLearnerByDemographicsRequest =
"""{
"givenName":"Darcie",
"familyName": "Tucker",
"dateOfBirth": "2024-01-01",
"gender": "TESTINGENUM",
"postcode": "CV49EE"
}"""
val actualResponse = webTestClient.post()
.uri("/learners")
.headers(setAuthorisation(roles = listOf("ROLE_LEARNER_RECORDS_SEARCH__RO")))
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(findLearnerByDemographicsRequest)
.exchange()
.expectStatus()
.isBadRequest
.expectBody()
.returnResult()
.responseBody

val actualResponseString = actualResponse?.toString(Charsets.UTF_8)
assertThat(actualResponseString).isEqualTo(gson.toJson(expectedResponse))
testExceptionHandling("/test/missing-mandatory-field", expectedResponse, expectedStatus = HttpStatus.BAD_REQUEST)
}

@Test
fun `should return No Resource errors when an unknown resource is called`() {
fun `should catch exceptions thrown when communicating with LRS (LRSException) and return Internal Server Error`() {
val expectedResponse = HmppsBoldLrsExceptionHandler.ErrorResponse(
HttpStatus.NOT_FOUND,
"No Resource Found",
"No resource found failure: No static resource someotherEndpoint.",
"Requested Resource not found on the server",
moreInfo = "Requested Resource not found on the server",
HttpStatus.INTERNAL_SERVER_ERROR,
"LRS Error",
"LRS returned an error without detail",
"LRS returned an error without detail",
"LRS Error",
)

val learnersRequest =
uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.LearnersRequest(
"Darcie",
"Tucker",
LocalDate.parse("2024-01-01"),
Gender.FEMALE,
"CV49EE",
)

val actualResponse = webTestClient.post()
.uri("/someotherEndpoint")
.headers(setAuthorisation(roles = listOf("ROLE_LEARNER_RECORDS_SEARCH__RO")))
.bodyValue(learnersRequest)
.accept(MediaType.parseMediaType("application/json"))
.exchange()
.expectStatus()
.isNotFound
.expectBody()
.returnResult()
.responseBody

val actualResponseString = actualResponse?.toString(Charsets.UTF_8)
assertThat(actualResponseString).isEqualTo(gson.toJson(expectedResponse))
testExceptionHandling("/test/lrs-error", expectedResponse, expectedStatus = HttpStatus.INTERNAL_SERVER_ERROR)
}

@Test
fun `should return bad request when a mandatory input is not provided`() {
fun `should catch forbidden exceptions (AccessDeniedException) and return FORBIDDEN`() {
val expectedResponse = HmppsBoldLrsExceptionHandler.ErrorResponse(
HttpStatus.BAD_REQUEST,
"Unreadable HTTP message",
"Unreadable HTTP message",
"JSON parse error: Instantiation of " +
"[simple type, class uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.LearnersRequest] " +
"value failed for JSON property givenName due to missing (therefore NULL) value " +
"for creator parameter givenName which is a non-nullable type",
"Unreadable HTTP message",
HttpStatus.FORBIDDEN,
"Forbidden - Access Denied",
"Forbidden: ",
"Forbidden - Access Denied",
"Forbidden - Access Denied",
)

val requestJsonWithoutGivenName = """
{
"lastName": "Tucker",
"dateOfBirth": "2024-01-01",
"gender": 1,
"postcode": "CV49EE"
}
"""
testExceptionHandling("/test/forbidden", expectedResponse, expectedStatus = HttpStatus.FORBIDDEN)
}

val actualResponse = webTestClient.post()
.uri("/learners")
.headers(setAuthorisation(roles = listOf("ROLE_LEARNER_RECORDS_SEARCH__RO")))
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestJsonWithoutGivenName)
.accept(MediaType.parseMediaType("application/json"))
.exchange()
.expectStatus()
.isBadRequest
.expectBody()
.returnResult()
.responseBody
@Test
fun `should catch generic exceptions and return Internal Server Error`() {
val expectedResponse = HmppsBoldLrsExceptionHandler.ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
"Unexpected error",
"Unexpected error: null",
"Unexpected error: null",
"Unexpected error",
)

val actualResponseString = actualResponse?.toString(Charsets.UTF_8)
assertThat(actualResponseString).isEqualTo(gson.toJson(expectedResponse))
testExceptionHandling("/test/generic-exception", expectedResponse, expectedStatus = HttpStatus.INTERNAL_SERVER_ERROR)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package uk.gov.justice.digital.hmpps.learnerrecordsapi.config

import com.google.gson.JsonParseException
import org.mockito.Mockito
import org.springframework.core.MethodParameter
import org.springframework.http.HttpInputMessage
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.validation.BindException
import org.springframework.validation.FieldError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.LRSException
import java.io.File

// Simply to assist in testing HmppsBoldLrsExceptionHandler, the endpoints are only visible to tests.

@RestController
class TestExceptionResource {

@PostMapping("/test/validation")
fun triggerValidationException(): Nothing = throw MethodArgumentNotValidException(
MethodParameter(this::class.java.getMethod("triggerValidationException"), -1),
BindException(Any(), "testObject").apply {
addError(FieldError("testObject", "testField", "Validation failed"))
},
)

@PostMapping("/test/missing-mandatory-field")
fun triggerMissingFieldException(): Nothing = throw HttpMessageNotReadableException(
"JSON parse error: Missing Field",
JsonParseException("Missing Field"),
Mockito.mock(HttpInputMessage::class.java),
)

@PostMapping("/test/lrs-error")
fun triggerLRSException(): Nothing = throw LRSException()

@PostMapping("/test/forbidden")
fun triggerForbiddenException(): Nothing = throw AccessDeniedException(file = File(""))

@PostMapping("/test/generic-exception")
fun triggerGenericException(): Nothing = throw Exception()
}
Loading

0 comments on commit 451351e

Please sign in to comment.