From e6869949cb617d3227433849881aba4bc89fc3de Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Thu, 16 Jan 2025 11:33:01 +0000 Subject: [PATCH 01/21] Publish Expression of Interest API --- .../v1/person/ExpressionInterestController.kt | 54 ++++++++ .../exception/MessageFailedException.kt | 3 + .../ExpressionInterest.kt | 7 ++ .../hmpps/ExpressionOfInterestMessage.kt | 6 + .../services/ExpressionInterestService.kt | 48 ++++++++ .../ExpressionInterestControllerTest.kt | 115 ++++++++++++++++++ .../person/AdjudicationsIntegrationTest.kt | 2 +- .../services/ExpressionInterestServiceTest.kt | 86 +++++++++++++ 8 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt new file mode 100644 index 000000000..a85c5e1a6 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -0,0 +1,54 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService + +@RestController +@RequestMapping("/v1/persons") +@Tag(name = "persons") +class ExpressionInterestController( + @Autowired val getPersonService: GetPersonService, + val expressionInterestService: ExpressionInterestService, +) { + @PutMapping("{hmppsId}/expression-of-interest/jobs/{jobid}") + @Operation( + summary = "Returns completed response", + responses = [ + ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully submitted an expression of interest"), + ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/Error"))]), + ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]), + ], + ) + fun submitExpressionOfInterest( + @Parameter(description = "A URL-encoded HMPPS identifier", example = "2008%2F0545166T") @PathVariable hmppsId: String, + @Parameter(description = "A job identifier") @PathVariable jobid: String, + ): ResponseEntity> { + val hmppsIdResponse = getPersonService.getCombinedDataForPerson(hmppsId) + + if (hmppsIdResponse.hasErrorCausedBy(UpstreamApiError.Type.ENTITY_NOT_FOUND, causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH)) { + return ResponseEntity.badRequest().build() + } + + val nomisHmppsId = hmppsIdResponse.data.probationOffenderSearch?.hmppsId + + expressionInterestService.sendExpressionOfInterest(ExpressionInterest(jobid, nomisHmppsId)) + + val responseBody = mapOf("message" to "Expression of interest submitted successfully") + return ResponseEntity.ok(responseBody) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt new file mode 100644 index 000000000..2f31f893f --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt @@ -0,0 +1,3 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception + +class MessageFailedException(msg: String) : RuntimeException(msg) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt new file mode 100644 index 000000000..2ee5873c7 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt @@ -0,0 +1,7 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest + +data class ExpressionInterest( + val jobId: String, + val hmppsId: String?, + val eventType: String = "ExpressionOfInterest", +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt new file mode 100644 index 000000000..6cd39e439 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps + +data class ExpressionOfInterestMessage( + val jobId: String, + val hmppsId: String, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt new file mode 100644 index 000000000..cfe765cda --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt @@ -0,0 +1,48 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Component +import org.springframework.stereotype.Service +import software.amazon.awssdk.services.sqs.model.SendMessageRequest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage +import uk.gov.justice.hmpps.sqs.HmppsQueue +import uk.gov.justice.hmpps.sqs.HmppsQueueService + +@Service +@Component +class ExpressionInterestService( + private val hmppsQueueService: HmppsQueueService, + private val objectMapper: ObjectMapper, +) { + private val eoiQueue by lazy { hmppsQueueService.findByQueueId("eoi-queue") as HmppsQueue } + private val eoiQueueSqsClient by lazy { eoiQueue.sqsClient } + private val eoiQueueUrl by lazy { eoiQueue.queueUrl } + + fun sendExpressionOfInterest(expressionInterest: ExpressionInterest) { + if (expressionInterest.hmppsId == null) { + println("HMPPS ID is null; skipping message sending.") + return + } + + try { + val messageBody = + objectMapper.writeValueAsString( + ExpressionOfInterestMessage( + jobId = expressionInterest.jobId, + hmppsId = expressionInterest.hmppsId, + ), + ) + + eoiQueueSqsClient.sendMessage( + SendMessageRequest.builder() + .queueUrl(eoiQueueUrl) + .messageBody(messageBody) + .build(), + ) + } catch (e: Exception) { + throw MessageFailedException("Failed to send message to SQS") + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt new file mode 100644 index 000000000..3d5d30f33 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -0,0 +1,115 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.mockito.Mockito +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.HttpStatus +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.MockMvc +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Identifiers +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.OffenderSearchResponse +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.PersonOnProbation +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.prisoneroffendersearch.POSPrisoner +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService + +@WebMvcTest(controllers = [ExpressionInterestController::class]) +@ActiveProfiles("test") +class ExpressionInterestControllerTest( + @Autowired private val springMockMvc: MockMvc, + @MockitoBean private val getPersonService: GetPersonService, + @MockitoBean private val expressionOfInterestService: ExpressionInterestService, +) : DescribeSpec({ + + val mockMvc = IntegrationAPIMockMvc(springMockMvc) + val basePath = "/v1/persons" + val validHmppsId = "1234ABC" + val invalidHmppsId = "INVALID_ID" + val jobId = "5678" + + describe("PUT $basePath/{hmppsId}/expression-of-interest/jobs/{jobId}") { + + beforeTest { + Mockito.reset(getPersonService, expressionOfInterestService) + } + + it("should return 201 Created when the expression of interest is successfully submitted") { + val personOnProbation = + PersonOnProbation( + person = + Person( + "Sam", + "Smith", + identifiers = Identifiers(nomisNumber = validHmppsId), + hmppsId = validHmppsId, + currentExclusion = true, + exclusionMessage = "An exclusion exists", + currentRestriction = false, + ), + underActiveSupervision = true, + ) + + val probationResponse = Response(data = personOnProbation, errors = emptyList()) + + val prisonOffenderSearch = POSPrisoner("Kim", "Kardashian") + val prisonResponse = Response(data = prisonOffenderSearch, errors = emptyList()) + + val offenderMap = + OffenderSearchResponse( + probationOffenderSearch = probationResponse.data, + prisonerOffenderSearch = prisonResponse.data.toPerson(), + ) + + whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenReturn( + Response(data = offenderMap, errors = emptyList()), + ) + + val result = mockMvc.performAuthorised("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") + + result.response.status.shouldBe(HttpStatus.CREATED.value()) + verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, validHmppsId)) + } + + it("should return 400 Bad Request when the HMPPS ID is not found") { + whenever(getPersonService.getCombinedDataForPerson(invalidHmppsId)).thenReturn( + Response( + data = + OffenderSearchResponse( + probationOffenderSearch = null, + prisonerOffenderSearch = null, + ), + errors = + listOf( + UpstreamApiError( + type = UpstreamApiError.Type.ENTITY_NOT_FOUND, + causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH, + ), + ), + ), + ) + + val result = mockMvc.performAuthorised("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") + + result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) + } + + it("should return 500 Internal Server Error when an unexpected error occurs") { + whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenThrow(RuntimeException("Unexpected error")) + + val result = mockMvc.performAuthorised("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") + + result.response.status.shouldBe(HttpStatus.INTERNAL_SERVER_ERROR.value()) + } + } + }) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/AdjudicationsIntegrationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/AdjudicationsIntegrationTest.kt index 7327e64db..952351131 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/AdjudicationsIntegrationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/AdjudicationsIntegrationTest.kt @@ -10,6 +10,6 @@ class AdjudicationsIntegrationTest : IntegrationTestBase() { fun `returns adjudications for a person`() { callApi("$basePath/$nomsId/reported-adjudications") .andExpect(status().isOk) - .andExpect(content().json(getExpectedResponse("person-adjudications"), true)) + .andExpect(content().json(getExpectedResponse("person-adjudications"))) } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt new file mode 100644 index 000000000..783506216 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt @@ -0,0 +1,86 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services + +import com.fasterxml.jackson.databind.ObjectMapper +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import software.amazon.awssdk.services.sqs.SqsAsyncClient +import software.amazon.awssdk.services.sqs.model.SendMessageRequest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage +import uk.gov.justice.hmpps.sqs.HmppsQueue +import uk.gov.justice.hmpps.sqs.HmppsQueueService + +class ExpressionInterestServiceTest : DescribeSpec({ + val mockQueueService = mock() + val mockSqsClient = mock() + val mockObjectMapper = mock() + + val eoiQueue = + mock { + on { sqsClient } doReturn mockSqsClient + on { queueUrl } doReturn "https://test-queue-url" + } + + val service = ExpressionInterestService(mockQueueService, mockObjectMapper) + + beforeTest { + reset(mockQueueService, mockSqsClient, mockObjectMapper) + whenever(mockQueueService.findByQueueId("eoi-queue")).thenReturn(eoiQueue) + } + + describe("sendExpressionOfInterest") { + it("should send a valid message successfully to SQS") { + val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") + val messageBody = """{"jobId":"12345","hmppsId":"H1234"}""" + + whenever(mockObjectMapper.writeValueAsString(any())) + .thenReturn(messageBody) + + service.sendExpressionOfInterest(expressionInterest) + + verify(mockSqsClient).sendMessage( + argThat { request: SendMessageRequest? -> + ( + request?.queueUrl() == "https://test-queue-url" && + request.messageBody() == messageBody + ) + }, + ) + } + + it("should throw MessageFailedException when SQS fails") { + val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") + + whenever(mockObjectMapper.writeValueAsString(any())) + .thenReturn("""{"jobId":"12345","hmppsId":"H1234"}""") + + whenever(mockSqsClient.sendMessage(any())) + .thenThrow(RuntimeException("Failed to send message to SQS")) + + val exception = + shouldThrow { + service.sendExpressionOfInterest(expressionInterest) + } + + exception.message shouldBe "Failed to send message to SQS" + } + + it("should prevent messages with null hmppsId being sent") { + val invalidExpressionInterest = ExpressionInterest(jobId = "12345", hmppsId = null) + + service.sendExpressionOfInterest(invalidExpressionInterest) + + verify(mockSqsClient, never()).sendMessage(any()) + } + } +}) From 09f206fd132b2dd44a74078e5ba183bbe63964a9 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Fri, 17 Jan 2025 09:09:55 +0000 Subject: [PATCH 02/21] Publish Expression of Interest API --- .../v1/person/ExpressionInterestController.kt | 30 ++++---- .../ExpressionInterest.kt | 3 +- .../hmpps/ExpressionOfInterestMessage.kt | 3 +- .../services/ExpressionInterestService.kt | 10 +-- .../ExpressionInterestControllerTest.kt | 77 ++++--------------- .../services/ExpressionInterestServiceTest.kt | 17 +--- 6 files changed, 39 insertions(+), 101 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index a85c5e1a6..bdf6638e4 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -6,15 +6,16 @@ import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService @@ -25,30 +26,29 @@ class ExpressionInterestController( @Autowired val getPersonService: GetPersonService, val expressionInterestService: ExpressionInterestService, ) { + val logger: Logger = LoggerFactory.getLogger(this::class.java) + @PutMapping("{hmppsId}/expression-of-interest/jobs/{jobid}") @Operation( summary = "Returns completed response", responses = [ - ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully submitted an expression of interest"), - ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/Error"))]), + ApiResponse(responseCode = "201", useReturnTypeSchema = true, description = "Successfully submitted an expression of interest"), + ApiResponse(responseCode = "403", content = [Content(schema = Schema(ref = "#/components/schemas/Error"))]), ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]), ], ) fun submitExpressionOfInterest( @Parameter(description = "A URL-encoded HMPPS identifier", example = "2008%2F0545166T") @PathVariable hmppsId: String, @Parameter(description = "A job identifier") @PathVariable jobid: String, - ): ResponseEntity> { - val hmppsIdResponse = getPersonService.getCombinedDataForPerson(hmppsId) + ): ResponseEntity { + try { + val nomisNumber = getPersonService.getNomisNumber(hmppsId).data?.nomisNumber ?: return ResponseEntity.badRequest().build() - if (hmppsIdResponse.hasErrorCausedBy(UpstreamApiError.Type.ENTITY_NOT_FOUND, causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH)) { - return ResponseEntity.badRequest().build() + expressionInterestService.sendExpressionOfInterest(ExpressionInterest(jobid, nomisNumber)) + return ResponseEntity.ok().build() + } catch (e: Exception) { + logger.info("${e.message}") + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() } - - val nomisHmppsId = hmppsIdResponse.data.probationOffenderSearch?.hmppsId - - expressionInterestService.sendExpressionOfInterest(ExpressionInterest(jobid, nomisHmppsId)) - - val responseBody = mapOf("message" to "Expression of interest submitted successfully") - return ResponseEntity.ok(responseBody) } } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt index 2ee5873c7..63cc35fdd 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt @@ -2,6 +2,5 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInte data class ExpressionInterest( val jobId: String, - val hmppsId: String?, - val eventType: String = "ExpressionOfInterest", + val nomisNumber: String, ) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt index 6cd39e439..cad5583a8 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt @@ -2,5 +2,6 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps data class ExpressionOfInterestMessage( val jobId: String, - val hmppsId: String, + val nomisNumber: String, + val eventType: String = "ExpressionOfInterest", ) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt index cfe765cda..022d2030b 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt @@ -1,6 +1,8 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services import com.fasterxml.jackson.databind.ObjectMapper +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import org.springframework.stereotype.Service import software.amazon.awssdk.services.sqs.model.SendMessageRequest @@ -16,22 +18,18 @@ class ExpressionInterestService( private val hmppsQueueService: HmppsQueueService, private val objectMapper: ObjectMapper, ) { + val logger: Logger = LoggerFactory.getLogger(this::class.java) private val eoiQueue by lazy { hmppsQueueService.findByQueueId("eoi-queue") as HmppsQueue } private val eoiQueueSqsClient by lazy { eoiQueue.sqsClient } private val eoiQueueUrl by lazy { eoiQueue.queueUrl } fun sendExpressionOfInterest(expressionInterest: ExpressionInterest) { - if (expressionInterest.hmppsId == null) { - println("HMPPS ID is null; skipping message sending.") - return - } - try { val messageBody = objectMapper.writeValueAsString( ExpressionOfInterestMessage( jobId = expressionInterest.jobId, - hmppsId = expressionInterest.hmppsId, + nomisNumber = expressionInterest.nomisNumber, ), ) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt index 3d5d30f33..71d5b4589 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -3,6 +3,7 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe import org.mockito.Mockito +import org.mockito.kotlin.doReturn import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired @@ -13,25 +14,18 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.MockMvc import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Identifiers -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.OffenderSearchResponse -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.PersonOnProbation +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.prisoneroffendersearch.POSPrisoner import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService @WebMvcTest(controllers = [ExpressionInterestController::class]) @ActiveProfiles("test") class ExpressionInterestControllerTest( - @Autowired private val springMockMvc: MockMvc, - @MockitoBean private val getPersonService: GetPersonService, - @MockitoBean private val expressionOfInterestService: ExpressionInterestService, + @Autowired var springMockMvc: MockMvc, + @MockitoBean val getPersonService: GetPersonService, + @MockitoBean val expressionOfInterestService: ExpressionInterestService, ) : DescribeSpec({ - val mockMvc = IntegrationAPIMockMvc(springMockMvc) val basePath = "/v1/persons" val validHmppsId = "1234ABC" @@ -39,74 +33,29 @@ class ExpressionInterestControllerTest( val jobId = "5678" describe("PUT $basePath/{hmppsId}/expression-of-interest/jobs/{jobId}") { - beforeTest { - Mockito.reset(getPersonService, expressionOfInterestService) + Mockito.reset(expressionOfInterestService, getPersonService) + + doReturn(Response(data = NomisNumber("nom1234"))) + .whenever(getPersonService).getNomisNumber(validHmppsId) } it("should return 201 Created when the expression of interest is successfully submitted") { - val personOnProbation = - PersonOnProbation( - person = - Person( - "Sam", - "Smith", - identifiers = Identifiers(nomisNumber = validHmppsId), - hmppsId = validHmppsId, - currentExclusion = true, - exclusionMessage = "An exclusion exists", - currentRestriction = false, - ), - underActiveSupervision = true, - ) - - val probationResponse = Response(data = personOnProbation, errors = emptyList()) - - val prisonOffenderSearch = POSPrisoner("Kim", "Kardashian") - val prisonResponse = Response(data = prisonOffenderSearch, errors = emptyList()) - - val offenderMap = - OffenderSearchResponse( - probationOffenderSearch = probationResponse.data, - prisonerOffenderSearch = prisonResponse.data.toPerson(), - ) - - whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenReturn( - Response(data = offenderMap, errors = emptyList()), - ) - + val testNomis = "nom1234" val result = mockMvc.performAuthorised("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - result.response.status.shouldBe(HttpStatus.CREATED.value()) - verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, validHmppsId)) + verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, testNomis)) + result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } it("should return 400 Bad Request when the HMPPS ID is not found") { - whenever(getPersonService.getCombinedDataForPerson(invalidHmppsId)).thenReturn( - Response( - data = - OffenderSearchResponse( - probationOffenderSearch = null, - prisonerOffenderSearch = null, - ), - errors = - listOf( - UpstreamApiError( - type = UpstreamApiError.Type.ENTITY_NOT_FOUND, - causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH, - ), - ), - ), - ) - val result = mockMvc.performAuthorised("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } it("should return 500 Internal Server Error when an unexpected error occurs") { - whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenThrow(RuntimeException("Unexpected error")) - + whenever(getPersonService.getNomisNumber(validHmppsId)).thenThrow(RuntimeException("Unexpected error")) val result = mockMvc.performAuthorised("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") result.response.status.shouldBe(HttpStatus.INTERNAL_SERVER_ERROR.value()) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt index 783506216..b51ea4390 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt @@ -8,7 +8,6 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argThat import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.reset import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -40,8 +39,8 @@ class ExpressionInterestServiceTest : DescribeSpec({ describe("sendExpressionOfInterest") { it("should send a valid message successfully to SQS") { - val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") - val messageBody = """{"jobId":"12345","hmppsId":"H1234"}""" + val expressionInterest = ExpressionInterest(jobId = "12345", nomisNumber = "H1234") + val messageBody = """{"jobId":"12345","nomisNumber":"H1234"}""" whenever(mockObjectMapper.writeValueAsString(any())) .thenReturn(messageBody) @@ -59,10 +58,10 @@ class ExpressionInterestServiceTest : DescribeSpec({ } it("should throw MessageFailedException when SQS fails") { - val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") + val expressionInterest = ExpressionInterest(jobId = "12345", nomisNumber = "H1234") whenever(mockObjectMapper.writeValueAsString(any())) - .thenReturn("""{"jobId":"12345","hmppsId":"H1234"}""") + .thenReturn("""{"jobId":"12345","nomisNumber":"H1234"}""") whenever(mockSqsClient.sendMessage(any())) .thenThrow(RuntimeException("Failed to send message to SQS")) @@ -74,13 +73,5 @@ class ExpressionInterestServiceTest : DescribeSpec({ exception.message shouldBe "Failed to send message to SQS" } - - it("should prevent messages with null hmppsId being sent") { - val invalidExpressionInterest = ExpressionInterest(jobId = "12345", hmppsId = null) - - service.sendExpressionOfInterest(invalidExpressionInterest) - - verify(mockSqsClient, never()).sendMessage(any()) - } } }) From e836116c40449d7b540b137df2e3a8ffc48676bc Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Fri, 17 Jan 2025 09:23:37 +0000 Subject: [PATCH 03/21] Publish Expression of Interest API --- .../hmppsintegrationapi/services/ExpressionInterestService.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt index 022d2030b..46888fde8 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt @@ -1,8 +1,6 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services import com.fasterxml.jackson.databind.ObjectMapper -import org.slf4j.Logger -import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import org.springframework.stereotype.Service import software.amazon.awssdk.services.sqs.model.SendMessageRequest @@ -18,7 +16,6 @@ class ExpressionInterestService( private val hmppsQueueService: HmppsQueueService, private val objectMapper: ObjectMapper, ) { - val logger: Logger = LoggerFactory.getLogger(this::class.java) private val eoiQueue by lazy { hmppsQueueService.findByQueueId("eoi-queue") as HmppsQueue } private val eoiQueueSqsClient by lazy { eoiQueue.sqsClient } private val eoiQueueUrl by lazy { eoiQueue.queueUrl } From a8455336555d3dac95f4fdbb7ce1a0b6df07e6c0 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Tue, 21 Jan 2025 10:56:53 +0000 Subject: [PATCH 04/21] refactor: use person.getComibinedData to verify hmppsId provided as part of the EOI --- .../v1/person/ExpressionInterestController.kt | 33 ++++-- .../ExpressionInterest.kt | 2 +- .../hmpps/ExpressionOfInterestMessage.kt | 2 +- .../services/ExpressionInterestService.kt | 2 +- src/main/resources/application-dev.yml | 2 + .../application-integration-test.yml | 1 + .../resources/application-local-docker.yml | 1 + src/main/resources/application-local.yml | 1 + src/main/resources/application-test.yml | 1 + .../ExpressionInterestControllerTest.kt | 107 +++++++++++++++--- .../helpers/IntegrationAPIMockMvc.kt | 5 + .../services/ExpressionInterestServiceTest.kt | 8 +- 12 files changed, 132 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index bdf6638e4..84dd6644d 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -2,8 +2,6 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import org.slf4j.Logger @@ -16,6 +14,10 @@ import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.OffenderSearchResponse +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService @@ -24,7 +26,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonServic @Tag(name = "persons") class ExpressionInterestController( @Autowired val getPersonService: GetPersonService, - val expressionInterestService: ExpressionInterestService, + @Autowired val expressionInterestService: ExpressionInterestService, ) { val logger: Logger = LoggerFactory.getLogger(this::class.java) @@ -32,9 +34,9 @@ class ExpressionInterestController( @Operation( summary = "Returns completed response", responses = [ - ApiResponse(responseCode = "201", useReturnTypeSchema = true, description = "Successfully submitted an expression of interest"), - ApiResponse(responseCode = "403", content = [Content(schema = Schema(ref = "#/components/schemas/Error"))]), - ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]), + ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully submitted an expression of interest"), + ApiResponse(responseCode = "403", useReturnTypeSchema = true, description = "Access is forbidden"), + ApiResponse(responseCode = "404", useReturnTypeSchema = true, description = "Not found"), ], ) fun submitExpressionOfInterest( @@ -42,13 +44,24 @@ class ExpressionInterestController( @Parameter(description = "A job identifier") @PathVariable jobid: String, ): ResponseEntity { try { - val nomisNumber = getPersonService.getNomisNumber(hmppsId).data?.nomisNumber ?: return ResponseEntity.badRequest().build() + val hmppsIdCheck = getPersonService.getCombinedDataForPerson(hmppsId) - expressionInterestService.sendExpressionOfInterest(ExpressionInterest(jobid, nomisNumber)) + if (hmppsIdCheck.hasErrorCausedBy(UpstreamApiError.Type.ENTITY_NOT_FOUND, causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH)) { + return ResponseEntity.badRequest().build() + } + + val verifiedNomisNumberId = getNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() + + expressionInterestService.sendExpressionOfInterest(ExpressionInterest(jobid, verifiedNomisNumberId)) return ResponseEntity.ok().build() } catch (e: Exception) { - logger.info("${e.message}") - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + logger.info("ExpressionInterestController: Unable to send message: ${e.message}") + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build() } } + + fun getNomisNumber(offenderSearchResponse: Response?): String? { + return offenderSearchResponse?.data?.probationOffenderSearch?.identifiers?.nomisNumber + ?: offenderSearchResponse?.data?.prisonerOffenderSearch?.identifiers?.nomisNumber + } } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt index 63cc35fdd..652400d50 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt @@ -2,5 +2,5 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInte data class ExpressionInterest( val jobId: String, - val nomisNumber: String, + val hmppsId: String?, ) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt index cad5583a8..87fdab384 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt @@ -2,6 +2,6 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps data class ExpressionOfInterestMessage( val jobId: String, - val nomisNumber: String, + val verifiedHmppsId: String?, val eventType: String = "ExpressionOfInterest", ) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt index 46888fde8..e52461428 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt @@ -26,7 +26,7 @@ class ExpressionInterestService( objectMapper.writeValueAsString( ExpressionOfInterestMessage( jobId = expressionInterest.jobId, - nomisNumber = expressionInterest.nomisNumber, + verifiedHmppsId = expressionInterest.hmppsId, ), ) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 3b49a99e1..a5a653124 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -54,6 +54,7 @@ authorisation: - "/v1/persons/.*/plp-induction-schedule" - "/v1/persons/.*/plp-induction-schedule/history" - "/v1/persons/.*/plp-review-schedule" + - "/v1/persons/[^/]+/expression-of-interest/jobs/[^/]+$" - "/v1/hmpps/reference-data" filters: ctrlo: @@ -149,6 +150,7 @@ authorisation: - "/v1/persons/.*/plp-induction-schedule" - "/v1/persons/.*/plp-induction-schedule/history" - "/v1/persons/.*/plp-review-schedule" + - "/v1/persons/[^/]+/expression-of-interest/jobs/[^/]+$" - "/v1/hmpps/id/by-nomis-number/[^/]*$" - "/v1/hmpps/id/nomis-number/by-hmpps-id/[^/]*$" filters: diff --git a/src/main/resources/application-integration-test.yml b/src/main/resources/application-integration-test.yml index 7fc948352..69933d6c4 100644 --- a/src/main/resources/application-integration-test.yml +++ b/src/main/resources/application-integration-test.yml @@ -73,6 +73,7 @@ authorisation: - "/v1/persons/.*/plp-induction-schedule" - "/v1/persons/.*/plp-induction-schedule/history" - "/v1/persons/.*/plp-review-schedule" + - "/v1/persons/.*/expression-of-interest/jobs/[^/]*$" - "/v1/epf/person-details/.*/[^/]*$" - "/v1/hmpps/id/nomis-number/[^/]*$" - "/v1/hmpps/id/by-nomis-number/[^/]*$" diff --git a/src/main/resources/application-local-docker.yml b/src/main/resources/application-local-docker.yml index ad241f809..1cac9fad8 100644 --- a/src/main/resources/application-local-docker.yml +++ b/src/main/resources/application-local-docker.yml @@ -47,6 +47,7 @@ authorisation: - "/v1/persons/.*/plp-induction-schedule" - "/v1/persons/.*/plp-induction-schedule/history" - "/v1/persons/.*/plp-review-schedule" + - "/v1/persons/.*/expression-of-interest/jobs/[^/]*$" - "/v1/epf/person-details/.*/[^/]*$" - "/v1/hmpps/id/nomis-number/[^/]*$" - "/v1/hmpps/id/by-nomis-number/[^/]*$" diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 8d0d65480..b9dfd509f 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -53,6 +53,7 @@ authorisation: - "/v1/persons/.*/cell-location" - "/v1/persons/.*/plp-induction-schedule" - "/v1/persons/.*/plp-review-schedule" + - "/v1/persons/[^/]+/expression-of-interest/jobs/[^/]+$" - "/v1/epf/person-details/.*/[^/]*$" - "/v1/hmpps/id/nomis-number/[^/]*$" - "/v1/hmpps/id/by-nomis-number/[^/]*$" diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index dc1ff7e99..43fa301a4 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -74,6 +74,7 @@ authorisation: - "/v1/persons/.*/plp-induction-schedule" - "/v1/persons/.*/plp-induction-schedule/history" - "/v1/persons/.*/plp-review-schedule" + - "/v1/persons/.*/expression-of-interest/jobs/[^/]*$" - "/v1/hmpps/id/nomis-number/[^/]*$" - "/v1/hmpps/id/.*/nomis-number" - "/v1/hmpps/id/by-nomis-number/[^/]*$" diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt index 71d5b4589..1ed49e2c7 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -1,9 +1,9 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.collections.shouldBeOneOf import io.kotest.matchers.shouldBe -import org.mockito.Mockito -import org.mockito.kotlin.doReturn +import org.mockito.Mockito.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired @@ -14,10 +14,15 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.MockMvc import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Identifiers +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.OffenderSearchResponse +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.PersonOnProbation import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.prisoneroffendersearch.POSPrisoner import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService +import java.time.LocalDate @WebMvcTest(controllers = [ExpressionInterestController::class]) @ActiveProfiles("test") @@ -29,36 +34,106 @@ class ExpressionInterestControllerTest( val mockMvc = IntegrationAPIMockMvc(springMockMvc) val basePath = "/v1/persons" val validHmppsId = "1234ABC" + val nomisId = "ABC789" + val prisonerNomis = "EFG7890" val invalidHmppsId = "INVALID_ID" val jobId = "5678" + val controller = ExpressionInterestController(mock(), mock()) describe("PUT $basePath/{hmppsId}/expression-of-interest/jobs/{jobId}") { beforeTest { - Mockito.reset(expressionOfInterestService, getPersonService) + val personOnProbation = + PersonOnProbation( + person = + Person( + "Sam", + "Smith", + identifiers = Identifiers(nomisNumber = nomisId), + hmppsId = validHmppsId, + currentExclusion = true, + exclusionMessage = "An exclusion exists", + currentRestriction = false, + ), + underActiveSupervision = true, + ) - doReturn(Response(data = NomisNumber("nom1234"))) - .whenever(getPersonService).getNomisNumber(validHmppsId) + val probationResponse = Response(data = personOnProbation, errors = emptyList()) + + val prisonOffenderSearch = POSPrisoner("Kim", "Kardashian", dateOfBirth = LocalDate.of(1992, 12, 3), prisonerNumber = nomisId) + val prisonResponse = Response(data = prisonOffenderSearch, errors = emptyList()) + + val offenderMap = + OffenderSearchResponse( + probationOffenderSearch = probationResponse.data, + prisonerOffenderSearch = prisonResponse.data.toPerson(), + ) + + whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenReturn( + Response(data = offenderMap, errors = emptyList()), + ) + } + + it("should return 200 OK and include verifiedNomisNumberId in the Expression of Interest") { + val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") + + verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, nomisId)) + result.response.status.shouldBe(HttpStatus.OK.value()) + } + + it("should return 404 when an invalid hmppsId has been submitted") { + val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") + + verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, nomisId)) + result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } - it("should return 201 Created when the expression of interest is successfully submitted") { - val testNomis = "nom1234" - val result = mockMvc.performAuthorised("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") + it("should return 400 Bad Request when both prison and probation nomis IDs do not exist") { + val emptyOffenderMap = + OffenderSearchResponse( + probationOffenderSearch = null, + prisonerOffenderSearch = null, + ) + whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenReturn( + Response(data = emptyOffenderMap, errors = emptyList()), + ) + + getPersonService.getCombinedDataForPerson(invalidHmppsId) + + val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") - verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, testNomis)) result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } - it("should return 400 Bad Request when the HMPPS ID is not found") { - val result = mockMvc.performAuthorised("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") + it("should return 400 Bad Request when probation nomis id does not exist and prisoner nomis id is not null") { + val prisonOffenderSearch = POSPrisoner("Kim", "Kardashian", dateOfBirth = LocalDate.of(1992, 12, 3), prisonerNumber = prisonerNomis) + val prisonResponse = Response(data = prisonOffenderSearch, errors = emptyList()) + val noProbationButPrisonerDataMap = + OffenderSearchResponse( + probationOffenderSearch = null, + prisonerOffenderSearch = prisonResponse.data.toPerson(), + ) + whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenReturn( + Response(data = noProbationButPrisonerDataMap, errors = emptyList()), + ) + + val hmppsIdCheck = getPersonService.getCombinedDataForPerson(validHmppsId) + val verifiedNomisNumberId = controller.getNomisNumber(hmppsIdCheck) + + verifiedNomisNumberId.shouldBe(prisonerNomis) + + val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } - it("should return 500 Internal Server Error when an unexpected error occurs") { - whenever(getPersonService.getNomisNumber(validHmppsId)).thenThrow(RuntimeException("Unexpected error")) - val result = mockMvc.performAuthorised("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") + it("should return 200 OK and when prisoner and probation nomis id exist to allow either of them") { + val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") + + val hmppsIdCheck = getPersonService.getCombinedDataForPerson(validHmppsId) + val verifiedNomisNumberId = controller.getNomisNumber(hmppsIdCheck) - result.response.status.shouldBe(HttpStatus.INTERNAL_SERVER_ERROR.value()) + verifiedNomisNumberId shouldBeOneOf listOf(nomisId, prisonerNomis) + result.response.status.shouldBe(HttpStatus.OK.value()) } } }) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/helpers/IntegrationAPIMockMvc.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/helpers/IntegrationAPIMockMvc.kt index 70f07d8d2..ac4ff3a55 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/helpers/IntegrationAPIMockMvc.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/helpers/IntegrationAPIMockMvc.kt @@ -15,6 +15,11 @@ class IntegrationAPIMockMvc( return mockMvc.perform(MockMvcRequestBuilders.get(path).header("subject-distinguished-name", subjectDistinguishedName)).andReturn() } + fun performAuthorisedPut(path: String): MvcResult { + val subjectDistinguishedName = "C=GB,ST=London,L=London,O=Home Office,CN=automated-test-client" + return mockMvc.perform(MockMvcRequestBuilders.put(path).header("subject-distinguished-name", subjectDistinguishedName)).andReturn() + } + fun performAuthorisedWithCN( path: String, cn: String, diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt index b51ea4390..fb2f2fae5 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt @@ -39,8 +39,8 @@ class ExpressionInterestServiceTest : DescribeSpec({ describe("sendExpressionOfInterest") { it("should send a valid message successfully to SQS") { - val expressionInterest = ExpressionInterest(jobId = "12345", nomisNumber = "H1234") - val messageBody = """{"jobId":"12345","nomisNumber":"H1234"}""" + val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") + val messageBody = """{"jobId":"12345","verifiedHmppsId":"H1234"}""" whenever(mockObjectMapper.writeValueAsString(any())) .thenReturn(messageBody) @@ -58,10 +58,10 @@ class ExpressionInterestServiceTest : DescribeSpec({ } it("should throw MessageFailedException when SQS fails") { - val expressionInterest = ExpressionInterest(jobId = "12345", nomisNumber = "H1234") + val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") whenever(mockObjectMapper.writeValueAsString(any())) - .thenReturn("""{"jobId":"12345","nomisNumber":"H1234"}""") + .thenReturn("""{"jobId":"12345","verifiedHmppsId":"H1234"}""") whenever(mockSqsClient.sendMessage(any())) .thenThrow(RuntimeException("Failed to send message to SQS")) From 6c095dee29d83baab90ee405a07dc4bb1a2987df Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Wed, 22 Jan 2025 15:05:23 +0000 Subject: [PATCH 05/21] rewrite test for nomischeck --- .../v1/person/ExpressionInterestController.kt | 22 ++-- .../ExpressionInterestControllerTest.kt | 109 ++++-------------- 2 files changed, 36 insertions(+), 95 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 84dd6644d..c25d4f824 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -14,9 +14,8 @@ import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.OffenderSearchResponse +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService @@ -44,15 +43,21 @@ class ExpressionInterestController( @Parameter(description = "A job identifier") @PathVariable jobid: String, ): ResponseEntity { try { - val hmppsIdCheck = getPersonService.getCombinedDataForPerson(hmppsId) + val hmppsIdCheck = getPersonService.getNomisNumber(hmppsId) - if (hmppsIdCheck.hasErrorCausedBy(UpstreamApiError.Type.ENTITY_NOT_FOUND, causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH)) { + if (hmppsIdCheck.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { + logger.info("Could not find nomis number for hmppsId: $hmppsId") return ResponseEntity.badRequest().build() } - val verifiedNomisNumberId = getNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() + if (hmppsIdCheck.hasError(UpstreamApiError.Type.BAD_REQUEST)) { + logger.info("Invalid hmppsId: $hmppsId") + return ResponseEntity.badRequest().build() + } + + val verifiedNomisNumber = getVerifiedNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() + expressionInterestService.sendExpressionOfInterest(ExpressionInterest(jobid, verifiedNomisNumber)) - expressionInterestService.sendExpressionOfInterest(ExpressionInterest(jobid, verifiedNomisNumberId)) return ResponseEntity.ok().build() } catch (e: Exception) { logger.info("ExpressionInterestController: Unable to send message: ${e.message}") @@ -60,8 +65,7 @@ class ExpressionInterestController( } } - fun getNomisNumber(offenderSearchResponse: Response?): String? { - return offenderSearchResponse?.data?.probationOffenderSearch?.identifiers?.nomisNumber - ?: offenderSearchResponse?.data?.prisonerOffenderSearch?.identifiers?.nomisNumber + fun getVerifiedNomisNumber(nomisNumberResponse: Response): String? { + return nomisNumberResponse.data?.nomisNumber } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt index 1ed49e2c7..02aa7544b 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -1,9 +1,7 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldBeOneOf import io.kotest.matchers.shouldBe -import org.mockito.Mockito.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired @@ -14,15 +12,10 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.MockMvc import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Identifiers -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.OffenderSearchResponse -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.PersonOnProbation +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.prisoneroffendersearch.POSPrisoner import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService -import java.time.LocalDate @WebMvcTest(controllers = [ExpressionInterestController::class]) @ActiveProfiles("test") @@ -33,107 +26,51 @@ class ExpressionInterestControllerTest( ) : DescribeSpec({ val mockMvc = IntegrationAPIMockMvc(springMockMvc) val basePath = "/v1/persons" - val validHmppsId = "1234ABC" - val nomisId = "ABC789" - val prisonerNomis = "EFG7890" + val validHmppsId = "AABCD1ABC" + val nomisId = "AABCD1ABC" val invalidHmppsId = "INVALID_ID" val jobId = "5678" - val controller = ExpressionInterestController(mock(), mock()) describe("PUT $basePath/{hmppsId}/expression-of-interest/jobs/{jobId}") { - beforeTest { - val personOnProbation = - PersonOnProbation( - person = - Person( - "Sam", - "Smith", - identifiers = Identifiers(nomisNumber = nomisId), - hmppsId = validHmppsId, - currentExclusion = true, - exclusionMessage = "An exclusion exists", - currentRestriction = false, - ), - underActiveSupervision = true, + it("should return 400 Bad Request if ENTITY_NOT_FOUND error occurs") { + val notFoundResponse = + Response( + data = null, + errors = emptyList(), ) + whenever(getPersonService.getNomisNumber(validHmppsId)).thenReturn(notFoundResponse) - val probationResponse = Response(data = personOnProbation, errors = emptyList()) - - val prisonOffenderSearch = POSPrisoner("Kim", "Kardashian", dateOfBirth = LocalDate.of(1992, 12, 3), prisonerNumber = nomisId) - val prisonResponse = Response(data = prisonOffenderSearch, errors = emptyList()) - - val offenderMap = - OffenderSearchResponse( - probationOffenderSearch = probationResponse.data, - prisonerOffenderSearch = prisonResponse.data.toPerson(), - ) - - whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenReturn( - Response(data = offenderMap, errors = emptyList()), - ) - } - - it("should return 200 OK and include verifiedNomisNumberId in the Expression of Interest") { val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, nomisId)) - result.response.status.shouldBe(HttpStatus.OK.value()) - } - - it("should return 404 when an invalid hmppsId has been submitted") { - val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") - - verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, nomisId)) result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } - it("should return 400 Bad Request when both prison and probation nomis IDs do not exist") { - val emptyOffenderMap = - OffenderSearchResponse( - probationOffenderSearch = null, - prisonerOffenderSearch = null, - ) - whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenReturn( - Response(data = emptyOffenderMap, errors = emptyList()), - ) - - getPersonService.getCombinedDataForPerson(invalidHmppsId) - + it("should return 400 if an invalid hmppsId is provided") { val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } - it("should return 400 Bad Request when probation nomis id does not exist and prisoner nomis id is not null") { - val prisonOffenderSearch = POSPrisoner("Kim", "Kardashian", dateOfBirth = LocalDate.of(1992, 12, 3), prisonerNumber = prisonerNomis) - val prisonResponse = Response(data = prisonOffenderSearch, errors = emptyList()) - - val noProbationButPrisonerDataMap = - OffenderSearchResponse( - probationOffenderSearch = null, - prisonerOffenderSearch = prisonResponse.data.toPerson(), + it("should return 200 OK on successful expression of interest submission") { + val validNomisResponse = + Response( + data = NomisNumber(nomisId), + errors = emptyList(), ) - whenever(getPersonService.getCombinedDataForPerson(validHmppsId)).thenReturn( - Response(data = noProbationButPrisonerDataMap, errors = emptyList()), - ) - - val hmppsIdCheck = getPersonService.getCombinedDataForPerson(validHmppsId) - val verifiedNomisNumberId = controller.getNomisNumber(hmppsIdCheck) + whenever(getPersonService.getNomisNumber(validHmppsId)).thenReturn(validNomisResponse) - verifiedNomisNumberId.shouldBe(prisonerNomis) + val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") - result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) + verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, nomisId)) + result.response.status.shouldBe(HttpStatus.OK.value()) } - it("should return 200 OK and when prisoner and probation nomis id exist to allow either of them") { - val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") + it("should return 400 Bad Request if an exception occurs") { + whenever(getPersonService.getNomisNumber(validHmppsId)).thenThrow(RuntimeException("Unexpected error")) - val hmppsIdCheck = getPersonService.getCombinedDataForPerson(validHmppsId) - val verifiedNomisNumberId = controller.getNomisNumber(hmppsIdCheck) + val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - verifiedNomisNumberId shouldBeOneOf listOf(nomisId, prisonerNomis) - result.response.status.shouldBe(HttpStatus.OK.value()) + result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } } }) From d1484925a9bd40f242beb798304dcaaf0d93a5f6 Mon Sep 17 00:00:00 2001 From: rickchoijd Date: Thu, 23 Jan 2025 11:33:24 +0000 Subject: [PATCH 06/21] [ESWE-1080] Revert unexpected test changes --- .../integration/person/AdjudicationsIntegrationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/AdjudicationsIntegrationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/AdjudicationsIntegrationTest.kt index 952351131..7327e64db 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/AdjudicationsIntegrationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/AdjudicationsIntegrationTest.kt @@ -10,6 +10,6 @@ class AdjudicationsIntegrationTest : IntegrationTestBase() { fun `returns adjudications for a person`() { callApi("$basePath/$nomsId/reported-adjudications") .andExpect(status().isOk) - .andExpect(content().json(getExpectedResponse("person-adjudications"))) + .andExpect(content().json(getExpectedResponse("person-adjudications"), true)) } } From a27465626cdb6f1b99265cb9fca7f0f967ec956d Mon Sep 17 00:00:00 2001 From: rickchoijd Date: Thu, 23 Jan 2025 11:53:41 +0000 Subject: [PATCH 07/21] [ESWE-1080] Fix lint-code formatting issues --- .../v1/person/ExpressionInterestController.kt | 4 +- .../exception/MessageFailedException.kt | 4 +- .../services/ExpressionInterestService.kt | 3 +- .../services/ExpressionInterestServiceTest.kt | 87 ++++++++++--------- 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index c25d4f824..6ef30a60c 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -65,7 +65,5 @@ class ExpressionInterestController( } } - fun getVerifiedNomisNumber(nomisNumberResponse: Response): String? { - return nomisNumberResponse.data?.nomisNumber - } + fun getVerifiedNomisNumber(nomisNumberResponse: Response) = nomisNumberResponse.data?.nomisNumber } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt index 2f31f893f..8947a6ee2 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt @@ -1,3 +1,5 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception -class MessageFailedException(msg: String) : RuntimeException(msg) +class MessageFailedException( + msg: String, +) : RuntimeException(msg) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt index e52461428..4af847ce5 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt @@ -31,7 +31,8 @@ class ExpressionInterestService( ) eoiQueueSqsClient.sendMessage( - SendMessageRequest.builder() + SendMessageRequest + .builder() .queueUrl(eoiQueueUrl) .messageBody(messageBody) .build(), diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt index fb2f2fae5..cadff8b2c 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt @@ -19,59 +19,60 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionO import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService -class ExpressionInterestServiceTest : DescribeSpec({ - val mockQueueService = mock() - val mockSqsClient = mock() - val mockObjectMapper = mock() +class ExpressionInterestServiceTest : + DescribeSpec({ + val mockQueueService = mock() + val mockSqsClient = mock() + val mockObjectMapper = mock() - val eoiQueue = - mock { - on { sqsClient } doReturn mockSqsClient - on { queueUrl } doReturn "https://test-queue-url" - } + val eoiQueue = + mock { + on { sqsClient } doReturn mockSqsClient + on { queueUrl } doReturn "https://test-queue-url" + } - val service = ExpressionInterestService(mockQueueService, mockObjectMapper) + val service = ExpressionInterestService(mockQueueService, mockObjectMapper) - beforeTest { - reset(mockQueueService, mockSqsClient, mockObjectMapper) - whenever(mockQueueService.findByQueueId("eoi-queue")).thenReturn(eoiQueue) - } + beforeTest { + reset(mockQueueService, mockSqsClient, mockObjectMapper) + whenever(mockQueueService.findByQueueId("eoi-queue")).thenReturn(eoiQueue) + } - describe("sendExpressionOfInterest") { - it("should send a valid message successfully to SQS") { - val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") - val messageBody = """{"jobId":"12345","verifiedHmppsId":"H1234"}""" + describe("sendExpressionOfInterest") { + it("should send a valid message successfully to SQS") { + val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") + val messageBody = """{"jobId":"12345","verifiedHmppsId":"H1234"}""" - whenever(mockObjectMapper.writeValueAsString(any())) - .thenReturn(messageBody) + whenever(mockObjectMapper.writeValueAsString(any())) + .thenReturn(messageBody) - service.sendExpressionOfInterest(expressionInterest) + service.sendExpressionOfInterest(expressionInterest) - verify(mockSqsClient).sendMessage( - argThat { request: SendMessageRequest? -> - ( - request?.queueUrl() == "https://test-queue-url" && - request.messageBody() == messageBody - ) - }, - ) - } + verify(mockSqsClient).sendMessage( + argThat { request: SendMessageRequest? -> + ( + request?.queueUrl() == "https://test-queue-url" && + request.messageBody() == messageBody + ) + }, + ) + } - it("should throw MessageFailedException when SQS fails") { - val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") + it("should throw MessageFailedException when SQS fails") { + val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") - whenever(mockObjectMapper.writeValueAsString(any())) - .thenReturn("""{"jobId":"12345","verifiedHmppsId":"H1234"}""") + whenever(mockObjectMapper.writeValueAsString(any())) + .thenReturn("""{"jobId":"12345","verifiedHmppsId":"H1234"}""") - whenever(mockSqsClient.sendMessage(any())) - .thenThrow(RuntimeException("Failed to send message to SQS")) + whenever(mockSqsClient.sendMessage(any())) + .thenThrow(RuntimeException("Failed to send message to SQS")) - val exception = - shouldThrow { - service.sendExpressionOfInterest(expressionInterest) - } + val exception = + shouldThrow { + service.sendExpressionOfInterest(expressionInterest) + } - exception.message shouldBe "Failed to send message to SQS" + exception.message shouldBe "Failed to send message to SQS" + } } - } -}) + }) From 8e98dd150c9f321e334a7ea2eb22ed0d3eb2e2dc Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Fri, 24 Jan 2025 07:09:56 +0000 Subject: [PATCH 08/21] update message properties --- .../controllers/v1/person/ExpressionInterestController.kt | 4 ++-- .../ExpressionOfInterest.kt} | 6 +++--- .../models/hmpps/ExpressionOfInterestMessage.kt | 7 +++++-- .../hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt | 5 +++++ .../services/ExpressionInterestService.kt | 8 ++++---- .../v1/person/ExpressionInterestControllerTest.kt | 4 ++-- 6 files changed, 21 insertions(+), 13 deletions(-) rename src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/{expressionOfInterest/ExpressionInterest.kt => hmpps/ExpressionOfInterest.kt} (53%) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 6ef30a60c..1d4ef84af 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError @@ -56,7 +56,7 @@ class ExpressionInterestController( } val verifiedNomisNumber = getVerifiedNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() - expressionInterestService.sendExpressionOfInterest(ExpressionInterest(jobid, verifiedNomisNumber)) + expressionInterestService.sendExpressionOfInterest(ExpressionOfInterest(jobid, verifiedNomisNumber)) return ResponseEntity.ok().build() } catch (e: Exception) { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterest.kt similarity index 53% rename from src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt rename to src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterest.kt index 652400d50..56ed39737 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/expressionOfInterest/ExpressionInterest.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterest.kt @@ -1,6 +1,6 @@ -package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps -data class ExpressionInterest( +data class ExpressionOfInterest( val jobId: String, - val hmppsId: String?, + val prisonNumber: String, ) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt index 87fdab384..c7f2112ac 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt @@ -1,7 +1,10 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps +import java.util.UUID + data class ExpressionOfInterestMessage( + val messageId: String = UUID.randomUUID().toString(), val jobId: String, - val verifiedHmppsId: String?, - val eventType: String = "ExpressionOfInterest", + val prisonNumber: String, + val eventType: MessageType = MessageType.EXPRESSION_OF_INTEREST_CREATED, ) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt new file mode 100644 index 000000000..5265d34ca --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt @@ -0,0 +1,5 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps + +enum class MessageType { + EXPRESSION_OF_INTEREST_CREATED, +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt index 4af847ce5..73a5c2521 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component import org.springframework.stereotype.Service import software.amazon.awssdk.services.sqs.model.SendMessageRequest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService @@ -20,13 +20,13 @@ class ExpressionInterestService( private val eoiQueueSqsClient by lazy { eoiQueue.sqsClient } private val eoiQueueUrl by lazy { eoiQueue.queueUrl } - fun sendExpressionOfInterest(expressionInterest: ExpressionInterest) { + fun sendExpressionOfInterest(expressionOfInterest: ExpressionOfInterest) { try { val messageBody = objectMapper.writeValueAsString( ExpressionOfInterestMessage( - jobId = expressionInterest.jobId, - verifiedHmppsId = expressionInterest.hmppsId, + jobId = expressionOfInterest.jobId, + prisonNumber = expressionOfInterest.prisonNumber, ), ) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt index 02aa7544b..c83c39972 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -11,7 +11,7 @@ import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.MockMvc import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService @@ -61,7 +61,7 @@ class ExpressionInterestControllerTest( val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionInterest(jobId, nomisId)) + verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionOfInterest(jobId, nomisId)) result.response.status.shouldBe(HttpStatus.OK.value()) } From 103a429c184e4064d6521c06565be6bc63a73787 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Fri, 24 Jan 2025 12:49:58 +0000 Subject: [PATCH 09/21] refactor components --- .../v1/person/ExpressionInterestController.kt | 6 +-- ...ice.kt => PutExpressionInterestService.kt} | 4 +- .../ExpressionInterestControllerTest.kt | 4 +- ...kt => PutExpressionInterestServiceTest.kt} | 44 ++++++++++++++----- 4 files changed, 40 insertions(+), 18 deletions(-) rename src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/{ExpressionInterestService.kt => PutExpressionInterestService.kt} (94%) rename src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/{ExpressionInterestServiceTest.kt => PutExpressionInterestServiceTest.kt} (57%) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 1d4ef84af..0e80020f4 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -17,15 +17,15 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionO import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.PutExpressionInterestService @RestController @RequestMapping("/v1/persons") @Tag(name = "persons") class ExpressionInterestController( @Autowired val getPersonService: GetPersonService, - @Autowired val expressionInterestService: ExpressionInterestService, + @Autowired val putExpressionInterestService: PutExpressionInterestService, ) { val logger: Logger = LoggerFactory.getLogger(this::class.java) @@ -56,7 +56,7 @@ class ExpressionInterestController( } val verifiedNomisNumber = getVerifiedNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() - expressionInterestService.sendExpressionOfInterest(ExpressionOfInterest(jobid, verifiedNomisNumber)) + putExpressionInterestService.sendExpressionOfInterest(ExpressionOfInterest(jobid, verifiedNomisNumber)) return ResponseEntity.ok().build() } catch (e: Exception) { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt similarity index 94% rename from src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt rename to src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt index 73a5c2521..b693af935 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -2,7 +2,6 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.stereotype.Component -import org.springframework.stereotype.Service import software.amazon.awssdk.services.sqs.model.SendMessageRequest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest @@ -10,9 +9,8 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionO import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService -@Service @Component -class ExpressionInterestService( +class PutExpressionInterestService( private val hmppsQueueService: HmppsQueueService, private val objectMapper: ObjectMapper, ) { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt index c83c39972..4e3cca6bd 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -14,15 +14,15 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMo import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.ExpressionInterestService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.PutExpressionInterestService @WebMvcTest(controllers = [ExpressionInterestController::class]) @ActiveProfiles("test") class ExpressionInterestControllerTest( @Autowired var springMockMvc: MockMvc, @MockitoBean val getPersonService: GetPersonService, - @MockitoBean val expressionOfInterestService: ExpressionInterestService, + @MockitoBean val expressionOfInterestService: PutExpressionInterestService, ) : DescribeSpec({ val mockMvc = IntegrationAPIMockMvc(springMockMvc) val basePath = "/v1/persons" diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt similarity index 57% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt index cadff8b2c..3d983373d 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/ExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -1,6 +1,7 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe @@ -14,12 +15,14 @@ import org.mockito.kotlin.whenever import software.amazon.awssdk.services.sqs.SqsAsyncClient import software.amazon.awssdk.services.sqs.model.SendMessageRequest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.expressionOfInterest.ExpressionInterest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.MockMvcExtensions.objectMapper +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.MessageType import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService -class ExpressionInterestServiceTest : +class PutExpressionInterestServiceTest : DescribeSpec({ val mockQueueService = mock() val mockSqsClient = mock() @@ -31,7 +34,7 @@ class ExpressionInterestServiceTest : on { queueUrl } doReturn "https://test-queue-url" } - val service = ExpressionInterestService(mockQueueService, mockObjectMapper) + val service = PutExpressionInterestService(mockQueueService, mockObjectMapper) beforeTest { reset(mockQueueService, mockSqsClient, mockObjectMapper) @@ -40,13 +43,19 @@ class ExpressionInterestServiceTest : describe("sendExpressionOfInterest") { it("should send a valid message successfully to SQS") { - val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") - val messageBody = """{"jobId":"12345","verifiedHmppsId":"H1234"}""" + val expressionOfInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") + val expectedMessage = + ExpressionOfInterestMessage( + jobId = "12345", + prisonNumber = "H1234", + eventType = MessageType.EXPRESSION_OF_INTEREST_CREATED, + ) + val messageBody = objectMapper.writeValueAsString(expectedMessage) whenever(mockObjectMapper.writeValueAsString(any())) .thenReturn(messageBody) - service.sendExpressionOfInterest(expressionInterest) + service.sendExpressionOfInterest(expressionOfInterest) verify(mockSqsClient).sendMessage( argThat { request: SendMessageRequest? -> @@ -59,10 +68,7 @@ class ExpressionInterestServiceTest : } it("should throw MessageFailedException when SQS fails") { - val expressionInterest = ExpressionInterest(jobId = "12345", hmppsId = "H1234") - - whenever(mockObjectMapper.writeValueAsString(any())) - .thenReturn("""{"jobId":"12345","verifiedHmppsId":"H1234"}""") + val expressionInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") whenever(mockSqsClient.sendMessage(any())) .thenThrow(RuntimeException("Failed to send message to SQS")) @@ -74,5 +80,23 @@ class ExpressionInterestServiceTest : exception.message shouldBe "Failed to send message to SQS" } + + it("should serialize ExpressionOfInterestMessage with correct keys") { + val expectedMessage = + ExpressionOfInterestMessage( + messageId = "1", + jobId = "12345", + prisonNumber = "H1234", + eventType = MessageType.EXPRESSION_OF_INTEREST_CREATED, + ) + + val serializedJson = objectMapper.writeValueAsString(expectedMessage) + val deserializedMap: Map = objectMapper.readValue(serializedJson) + + assert(deserializedMap.containsKey("messageId")) + assert(deserializedMap.containsKey("jobId")) + assert(deserializedMap.containsKey("prisonNumber")) + assert(deserializedMap.containsKey("eventType")) + } } }) From cb8ed69ee61bf686974d53bae66142c9f2d81763 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Fri, 24 Jan 2025 14:20:41 +0000 Subject: [PATCH 10/21] formatting --- .../models/hmpps/ExpressionOfInterestMessage.kt | 12 +++++++----- .../hmppsintegrationapi/models/hmpps/MessageType.kt | 5 ----- .../services/PutExpressionInterestService.kt | 2 ++ .../services/PutExpressionInterestServiceTest.kt | 11 ++++++++--- 4 files changed, 17 insertions(+), 13 deletions(-) delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt index c7f2112ac..903a29c52 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt @@ -1,10 +1,12 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps -import java.util.UUID - data class ExpressionOfInterestMessage( - val messageId: String = UUID.randomUUID().toString(), + val messageId: String, val jobId: String, val prisonNumber: String, - val eventType: MessageType = MessageType.EXPRESSION_OF_INTEREST_CREATED, -) + val eventType: EventType = EventType.EXPRESSION_OF_INTEREST_MESSAGE_CREATED, +) { + enum class EventType { + EXPRESSION_OF_INTEREST_MESSAGE_CREATED, + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt deleted file mode 100644 index 5265d34ca..000000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/MessageType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps - -enum class MessageType { - EXPRESSION_OF_INTEREST_CREATED, -} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt index b693af935..7ce34c6d7 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -8,6 +8,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionO import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService +import java.util.UUID @Component class PutExpressionInterestService( @@ -23,6 +24,7 @@ class PutExpressionInterestService( val messageBody = objectMapper.writeValueAsString( ExpressionOfInterestMessage( + messageId = UUID.randomUUID().toString(), jobId = expressionOfInterest.jobId, prisonNumber = expressionOfInterest.prisonNumber, ), diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt index 3d983373d..70c7a3fe4 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -18,9 +18,9 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedE import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.MockMvcExtensions.objectMapper import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.MessageType import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService +import kotlin.test.assertEquals class PutExpressionInterestServiceTest : DescribeSpec({ @@ -46,9 +46,9 @@ class PutExpressionInterestServiceTest : val expressionOfInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") val expectedMessage = ExpressionOfInterestMessage( + messageId = "1", jobId = "12345", prisonNumber = "H1234", - eventType = MessageType.EXPRESSION_OF_INTEREST_CREATED, ) val messageBody = objectMapper.writeValueAsString(expectedMessage) @@ -87,16 +87,21 @@ class PutExpressionInterestServiceTest : messageId = "1", jobId = "12345", prisonNumber = "H1234", - eventType = MessageType.EXPRESSION_OF_INTEREST_CREATED, ) val serializedJson = objectMapper.writeValueAsString(expectedMessage) + val deserializedMap: Map = objectMapper.readValue(serializedJson) + val eventType = deserializedMap["eventType"] assert(deserializedMap.containsKey("messageId")) assert(deserializedMap.containsKey("jobId")) assert(deserializedMap.containsKey("prisonNumber")) assert(deserializedMap.containsKey("eventType")) + assertEquals( + expected = ExpressionOfInterestMessage.EventType.EXPRESSION_OF_INTEREST_MESSAGE_CREATED.name, + actual = eventType, + ) } } }) From 7f529a54044ee20ccb9c641cffd535a51cbf8c18 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Fri, 24 Jan 2025 15:56:36 +0000 Subject: [PATCH 11/21] remove path in mapps section --- src/main/resources/application-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index a5a653124..2a6ec2fc1 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -54,7 +54,6 @@ authorisation: - "/v1/persons/.*/plp-induction-schedule" - "/v1/persons/.*/plp-induction-schedule/history" - "/v1/persons/.*/plp-review-schedule" - - "/v1/persons/[^/]+/expression-of-interest/jobs/[^/]+$" - "/v1/hmpps/reference-data" filters: ctrlo: From 2888b7cd4600c7dd42cd6c2e474fa84f78a5dc86 Mon Sep 17 00:00:00 2001 From: rickchoijd Date: Tue, 28 Jan 2025 11:30:09 +0000 Subject: [PATCH 12/21] [ESWE-1080] Fixes around missing queue 1) add back missing queue for local run 2) add back error log (cause) 3) add back missing `eventType` (message attribute) 4) rename queueId --- .../controllers/v1/person/ExpressionInterestController.kt | 2 +- .../hmppsintegrationapi/exception/MessageFailedException.kt | 3 ++- .../services/PutExpressionInterestService.kt | 6 ++++-- src/main/resources/application-local.yml | 2 ++ .../services/PutExpressionInterestServiceTest.kt | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 0e80020f4..d4b239a90 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -60,7 +60,7 @@ class ExpressionInterestController( return ResponseEntity.ok().build() } catch (e: Exception) { - logger.info("ExpressionInterestController: Unable to send message: ${e.message}") + logger.error("ExpressionInterestController: Unable to send message: ${e.message}", e) return ResponseEntity.status(HttpStatus.BAD_REQUEST).build() } } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt index 8947a6ee2..2c1e75a2a 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt @@ -2,4 +2,5 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception class MessageFailedException( msg: String, -) : RuntimeException(msg) + cause: Throwable? = null, +) : RuntimeException(msg, cause) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt index 7ce34c6d7..6fb8c9259 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -8,6 +8,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionO import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService +import uk.gov.justice.hmpps.sqs.eventTypeMessageAttributes import java.util.UUID @Component @@ -15,7 +16,7 @@ class PutExpressionInterestService( private val hmppsQueueService: HmppsQueueService, private val objectMapper: ObjectMapper, ) { - private val eoiQueue by lazy { hmppsQueueService.findByQueueId("eoi-queue") as HmppsQueue } + private val eoiQueue by lazy { hmppsQueueService.findByQueueId("jobsboardintegration") as HmppsQueue } private val eoiQueueSqsClient by lazy { eoiQueue.sqsClient } private val eoiQueueUrl by lazy { eoiQueue.queueUrl } @@ -35,10 +36,11 @@ class PutExpressionInterestService( .builder() .queueUrl(eoiQueueUrl) .messageBody(messageBody) + .eventTypeMessageAttributes("mjma-jobs-board.job.expression-of-interest.created") .build(), ) } catch (e: Exception) { - throw MessageFailedException("Failed to send message to SQS") + throw MessageFailedException("Failed to send message to SQS", e) } } } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index b9dfd509f..79d0632ef 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -19,6 +19,8 @@ hmpps.sqs: queues: audit: queueName: "audit" + jobsboardintegration: + queueName: "jobsboard-integration" authorisation: consumers: diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt index 70c7a3fe4..76e911804 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -38,7 +38,7 @@ class PutExpressionInterestServiceTest : beforeTest { reset(mockQueueService, mockSqsClient, mockObjectMapper) - whenever(mockQueueService.findByQueueId("eoi-queue")).thenReturn(eoiQueue) + whenever(mockQueueService.findByQueueId("jobsboardintegration")).thenReturn(eoiQueue) } describe("sendExpressionOfInterest") { From c5614164dad44e71e0c59fa52c16d14fe1b754d8 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Tue, 28 Jan 2025 11:36:57 +0000 Subject: [PATCH 13/21] add sqs name in yml --- .../controllers/v1/person/ExpressionInterestController.kt | 4 ++-- src/main/resources/application-integration-test.yml | 2 ++ src/main/resources/application-local-docker.yml | 2 ++ src/main/resources/application-local.yml | 3 +++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 0e80020f4..641d7ea0a 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -46,12 +46,12 @@ class ExpressionInterestController( val hmppsIdCheck = getPersonService.getNomisNumber(hmppsId) if (hmppsIdCheck.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { - logger.info("Could not find nomis number for hmppsId: $hmppsId") + logger.info("ExpressionInterestController: Could not find nomis number for hmppsId: $hmppsId") return ResponseEntity.badRequest().build() } if (hmppsIdCheck.hasError(UpstreamApiError.Type.BAD_REQUEST)) { - logger.info("Invalid hmppsId: $hmppsId") + logger.info("ExpressionInterestController: Invalid hmppsId: $hmppsId") return ResponseEntity.badRequest().build() } diff --git a/src/main/resources/application-integration-test.yml b/src/main/resources/application-integration-test.yml index 69933d6c4..2bfb6ed52 100644 --- a/src/main/resources/application-integration-test.yml +++ b/src/main/resources/application-integration-test.yml @@ -38,6 +38,8 @@ hmpps.sqs: queues: audit: queueName: "audit" + eoi: + queueName: "eoi-queue" authorisation: consumers: diff --git a/src/main/resources/application-local-docker.yml b/src/main/resources/application-local-docker.yml index 1cac9fad8..7d44836aa 100644 --- a/src/main/resources/application-local-docker.yml +++ b/src/main/resources/application-local-docker.yml @@ -80,3 +80,5 @@ hmpps.sqs: queues: audit: queueName: "audit" + eoi: + queueName: "eoi-queue" diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index b9dfd509f..5c8313d99 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -19,6 +19,9 @@ hmpps.sqs: queues: audit: queueName: "audit" + eoi: + queueName: "eoi-queue" + authorisation: consumers: From 177fe71884d833264e2e25f01bfe67f928f63d82 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Tue, 28 Jan 2025 13:53:21 +0000 Subject: [PATCH 14/21] correct tests and controller --- .../v1/person/ExpressionInterestController.kt | 3 ++- .../v1/person/ExpressionInterestControllerTest.kt | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 481dd16d7..37aa9cc33 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -35,6 +35,7 @@ class ExpressionInterestController( responses = [ ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully submitted an expression of interest"), ApiResponse(responseCode = "403", useReturnTypeSchema = true, description = "Access is forbidden"), + ApiResponse(responseCode = "400", useReturnTypeSchema = true, description = "Bade Request"), ApiResponse(responseCode = "404", useReturnTypeSchema = true, description = "Not found"), ], ) @@ -47,7 +48,7 @@ class ExpressionInterestController( if (hmppsIdCheck.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { logger.info("ExpressionInterestController: Could not find nomis number for hmppsId: $hmppsId") - return ResponseEntity.badRequest().build() + return ResponseEntity.notFound().build() } if (hmppsIdCheck.hasError(UpstreamApiError.Type.BAD_REQUEST)) { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt index 4e3cca6bd..91a9351e9 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -14,6 +14,8 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMo import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.PutExpressionInterestService @@ -32,17 +34,24 @@ class ExpressionInterestControllerTest( val jobId = "5678" describe("PUT $basePath/{hmppsId}/expression-of-interest/jobs/{jobId}") { - it("should return 400 Bad Request if ENTITY_NOT_FOUND error occurs") { + it("should return 404 Not Found if ENTITY_NOT_FOUND error occurs") { val notFoundResponse = Response( data = null, - errors = emptyList(), + errors = + listOf( + UpstreamApiError( + type = UpstreamApiError.Type.ENTITY_NOT_FOUND, + description = "Entity not found", + causedBy = UpstreamApi.PRISONER_OFFENDER_SEARCH, + ), + ), ) whenever(getPersonService.getNomisNumber(validHmppsId)).thenReturn(notFoundResponse) val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) + result.response.status.shouldBe(HttpStatus.NOT_FOUND.value()) } it("should return 400 if an invalid hmppsId is provided") { From 75dda34bb89f44de2a9c951d0f308066a52cd95c Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Wed, 5 Feb 2025 16:28:43 +0000 Subject: [PATCH 15/21] refactor with generic message --- .../HmppsIntegrationApiExceptionHandler.kt | 15 ++++ .../v1/person/ExpressionInterestController.kt | 34 ++++----- .../hmpps/ExpressionOfInterestMessage.kt | 12 --- .../models/hmpps/HmppsMessage.kt | 13 ++++ .../models/hmpps/HmppsMessageEventType.kt | 5 ++ .../services/PutExpressionInterestService.kt | 17 +++-- .../ExpressionInterestControllerTest.kt | 23 ++++-- .../PutExpressionInterestServiceTest.kt | 73 +++++++++++++------ 8 files changed, 127 insertions(+), 65 deletions(-) delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/config/HmppsIntegrationApiExceptionHandler.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/config/HmppsIntegrationApiExceptionHandler.kt index 76a2639e8..73ce594f8 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/config/HmppsIntegrationApiExceptionHandler.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/config/HmppsIntegrationApiExceptionHandler.kt @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.reactive.function.client.WebClientResponseException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.AuthenticationFailedException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException @RestControllerAdvice class HmppsIntegrationApiExceptionHandler { @@ -87,6 +88,20 @@ class HmppsIntegrationApiExceptionHandler { ) } + @ExceptionHandler(MessageFailedException::class) + fun handleMessageFailedExceptionException(e: MessageFailedException): ResponseEntity? { + logAndCapture("Unable to send message to SQS: {}", e) + return ResponseEntity + .status(INTERNAL_SERVER_ERROR) + .body( + ErrorResponse( + status = INTERNAL_SERVER_ERROR, + developerMessage = "Unable to send message to SQS", + userMessage = e.message, + ), + ) + } + private fun logAndCapture( message: String, e: Exception, diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 37aa9cc33..b42aa06f6 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -4,15 +4,16 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.ValidationException import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response @@ -35,7 +36,7 @@ class ExpressionInterestController( responses = [ ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully submitted an expression of interest"), ApiResponse(responseCode = "403", useReturnTypeSchema = true, description = "Access is forbidden"), - ApiResponse(responseCode = "400", useReturnTypeSchema = true, description = "Bade Request"), + ApiResponse(responseCode = "400", useReturnTypeSchema = true, description = "Bad Request"), ApiResponse(responseCode = "404", useReturnTypeSchema = true, description = "Not found"), ], ) @@ -43,27 +44,22 @@ class ExpressionInterestController( @Parameter(description = "A URL-encoded HMPPS identifier", example = "2008%2F0545166T") @PathVariable hmppsId: String, @Parameter(description = "A job identifier") @PathVariable jobid: String, ): ResponseEntity { - try { - val hmppsIdCheck = getPersonService.getNomisNumber(hmppsId) + val hmppsIdCheck = getPersonService.getNomisNumber(hmppsId) - if (hmppsIdCheck.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { - logger.info("ExpressionInterestController: Could not find nomis number for hmppsId: $hmppsId") - return ResponseEntity.notFound().build() - } + if (hmppsIdCheck.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { + logger.info("ExpressionInterestController: Could not find nomis number for hmppsId: $hmppsId") + throw EntityNotFoundException("") + } - if (hmppsIdCheck.hasError(UpstreamApiError.Type.BAD_REQUEST)) { - logger.info("ExpressionInterestController: Invalid hmppsId: $hmppsId") - return ResponseEntity.badRequest().build() - } + if (hmppsIdCheck.hasError(UpstreamApiError.Type.BAD_REQUEST)) { + logger.info("ExpressionInterestController: Invalid hmppsId: $hmppsId") + throw ValidationException("") + } - val verifiedNomisNumber = getVerifiedNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() - putExpressionInterestService.sendExpressionOfInterest(ExpressionOfInterest(jobid, verifiedNomisNumber)) + val verifiedNomisNumber = getVerifiedNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() + putExpressionInterestService.sendExpressionOfInterest(ExpressionOfInterest(jobid, verifiedNomisNumber)) - return ResponseEntity.ok().build() - } catch (e: Exception) { - logger.error("ExpressionInterestController: Unable to send message: ${e.message}", e) - return ResponseEntity.status(HttpStatus.BAD_REQUEST).build() - } + return ResponseEntity.ok().build() } fun getVerifiedNomisNumber(nomisNumberResponse: Response) = nomisNumberResponse.data?.nomisNumber diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt deleted file mode 100644 index 903a29c52..000000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterestMessage.kt +++ /dev/null @@ -1,12 +0,0 @@ -package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps - -data class ExpressionOfInterestMessage( - val messageId: String, - val jobId: String, - val prisonNumber: String, - val eventType: EventType = EventType.EXPRESSION_OF_INTEREST_MESSAGE_CREATED, -) { - enum class EventType { - EXPRESSION_OF_INTEREST_MESSAGE_CREATED, - } -} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt new file mode 100644 index 000000000..4c3ad1fd3 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt @@ -0,0 +1,13 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps + +data class HmppsMessage( + val messageId: String, + val eventType: String, + val description: String? = null, + val messageAttributes: Map = emptyMap(), +) + +data class MessageAttribute( + val value: String, + val type: String, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt new file mode 100644 index 000000000..1e44c0f16 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt @@ -0,0 +1,5 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps + +enum class HmppsMessageEventType { + EXPRESSION_OF_INTEREST_CREATED, +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt index 6fb8c9259..d36f6d4e8 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -5,7 +5,8 @@ import org.springframework.stereotype.Component import software.amazon.awssdk.services.sqs.model.SendMessageRequest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsMessage +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsMessageEventType import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService import uk.gov.justice.hmpps.sqs.eventTypeMessageAttributes @@ -22,12 +23,16 @@ class PutExpressionInterestService( fun sendExpressionOfInterest(expressionOfInterest: ExpressionOfInterest) { try { - val messageBody = + val hmppsMessage = objectMapper.writeValueAsString( - ExpressionOfInterestMessage( + HmppsMessage( messageId = UUID.randomUUID().toString(), - jobId = expressionOfInterest.jobId, - prisonNumber = expressionOfInterest.prisonNumber, + eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED.name, + messageAttributes = + mapOf( + "jobId" to expressionOfInterest.jobId, + "prisonNumber" to expressionOfInterest.prisonNumber, + ), ), ) @@ -35,7 +40,7 @@ class PutExpressionInterestService( SendMessageRequest .builder() .queueUrl(eoiQueueUrl) - .messageBody(messageBody) + .messageBody(hmppsMessage) .eventTypeMessageAttributes("mjma-jobs-board.job.expression-of-interest.created") .build(), ) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt index 91a9351e9..6c2f8f9cb 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -50,13 +50,25 @@ class ExpressionInterestControllerTest( whenever(getPersonService.getNomisNumber(validHmppsId)).thenReturn(notFoundResponse) val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - result.response.status.shouldBe(HttpStatus.NOT_FOUND.value()) } - it("should return 400 if an invalid hmppsId is provided") { - val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") + it("should throw ValidationException if an invalid hmppsId is provided") { + val invalidResponse = + Response( + data = null, + errors = + listOf( + UpstreamApiError( + type = UpstreamApiError.Type.BAD_REQUEST, + description = "Invalid HmppsId", + causedBy = UpstreamApi.NOMIS, + ), + ), + ) + whenever(getPersonService.getNomisNumber(invalidHmppsId)).thenReturn(invalidResponse) + val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } @@ -74,12 +86,11 @@ class ExpressionInterestControllerTest( result.response.status.shouldBe(HttpStatus.OK.value()) } - it("should return 400 Bad Request if an exception occurs") { + it("should return 500 Server Error if an exception occurs") { whenever(getPersonService.getNomisNumber(validHmppsId)).thenThrow(RuntimeException("Unexpected error")) val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - - result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) + result.response.status.shouldBe(HttpStatus.INTERNAL_SERVER_ERROR.value()) } } }) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt index 76e911804..1cc579439 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -17,7 +17,8 @@ import software.amazon.awssdk.services.sqs.model.SendMessageRequest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.MockMvcExtensions.objectMapper import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterestMessage +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsMessage +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsMessageEventType import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService import kotlin.test.assertEquals @@ -25,8 +26,8 @@ import kotlin.test.assertEquals class PutExpressionInterestServiceTest : DescribeSpec({ val mockQueueService = mock() - val mockSqsClient = mock() val mockObjectMapper = mock() + val mockSqsClient = mock() val eoiQueue = mock { @@ -34,35 +35,28 @@ class PutExpressionInterestServiceTest : on { queueUrl } doReturn "https://test-queue-url" } + val queId = "jobsboardintegration" val service = PutExpressionInterestService(mockQueueService, mockObjectMapper) beforeTest { reset(mockQueueService, mockSqsClient, mockObjectMapper) - whenever(mockQueueService.findByQueueId("jobsboardintegration")).thenReturn(eoiQueue) + whenever(mockQueueService.findByQueueId(queId)).thenReturn(eoiQueue) } describe("sendExpressionOfInterest") { it("should send a valid message successfully to SQS") { val expressionOfInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") - val expectedMessage = - ExpressionOfInterestMessage( - messageId = "1", - jobId = "12345", - prisonNumber = "H1234", - ) - val messageBody = objectMapper.writeValueAsString(expectedMessage) + val messageBody = """{"messageId":"1","eventType":"EXPRESSION_OF_INTEREST_CREATED","messageAttributes":{"jobId":"12345","prisonNumber":"H1234"}}""" - whenever(mockObjectMapper.writeValueAsString(any())) + whenever(mockObjectMapper.writeValueAsString(any())) .thenReturn(messageBody) service.sendExpressionOfInterest(expressionOfInterest) verify(mockSqsClient).sendMessage( argThat { request: SendMessageRequest? -> - ( - request?.queueUrl() == "https://test-queue-url" && - request.messageBody() == messageBody - ) + request?.queueUrl() == "https://test-queue-url" && + request.messageBody() == messageBody }, ) } @@ -83,25 +77,60 @@ class PutExpressionInterestServiceTest : it("should serialize ExpressionOfInterestMessage with correct keys") { val expectedMessage = - ExpressionOfInterestMessage( + HmppsMessage( messageId = "1", - jobId = "12345", - prisonNumber = "H1234", + eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED.name, + messageAttributes = + mapOf( + "jobId" to "12345", + "prisonNumber" to "H1234", + ), ) val serializedJson = objectMapper.writeValueAsString(expectedMessage) val deserializedMap: Map = objectMapper.readValue(serializedJson) val eventType = deserializedMap["eventType"] - assert(deserializedMap.containsKey("messageId")) - assert(deserializedMap.containsKey("jobId")) - assert(deserializedMap.containsKey("prisonNumber")) + assert(deserializedMap.containsKey("messageAttributes")) assert(deserializedMap.containsKey("eventType")) assertEquals( - expected = ExpressionOfInterestMessage.EventType.EXPRESSION_OF_INTEREST_MESSAGE_CREATED.name, + expected = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED.name, actual = eventType, ) + + val messageAttributes = deserializedMap["messageAttributes"] as? Map + messageAttributes?.containsKey("jobId")?.let { assert(it) } + messageAttributes?.containsKey("prisonNumber")?.let { assert(it) } + } + + it("allows messages of any type and anybody to be sent") { + val externalType = "externalType" + + val expectedMessage = + HmppsMessage( + messageId = "1", + eventType = externalType, + messageAttributes = + mapOf( + "personId" to "12345", + "entityNumber" to "H1234", + ), + ) + + val serializedJson = objectMapper.writeValueAsString(expectedMessage) + + val deserializedMap: Map = objectMapper.readValue(serializedJson) + val eventType = deserializedMap["eventType"] + + assertEquals( + expected = externalType, + actual = eventType, + ) + + val messageAttributes = deserializedMap["messageAttributes"] as? Map + messageAttributes?.containsKey("personId")?.let { assert(it) } + messageAttributes?.containsKey("entityNumber")?.let { assert(it) } } } }) From 91d49aa09392cee25493bfef3492db03edc3b767 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Thu, 6 Feb 2025 12:03:47 +0000 Subject: [PATCH 16/21] add new tests for event type --- .../models/hmpps/HmppsMessage.kt | 5 --- .../models/hmpps/HmppsMessageEventType.kt | 2 +- .../services/PutExpressionInterestService.kt | 2 +- .../PutExpressionInterestServiceTest.kt | 39 ++++++++++++++++++- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt index 4c3ad1fd3..1542f2712 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt @@ -6,8 +6,3 @@ data class HmppsMessage( val description: String? = null, val messageAttributes: Map = emptyMap(), ) - -data class MessageAttribute( - val value: String, - val type: String, -) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt index 1e44c0f16..664d7deee 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt @@ -1,5 +1,5 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps enum class HmppsMessageEventType { - EXPRESSION_OF_INTEREST_CREATED, + ExpressionOfInterestCreated, } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt index d36f6d4e8..3f5dec43f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -27,7 +27,7 @@ class PutExpressionInterestService( objectMapper.writeValueAsString( HmppsMessage( messageId = UUID.randomUUID().toString(), - eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED.name, + eventType = HmppsMessageEventType.ExpressionOfInterestCreated.name, messageAttributes = mapOf( "jobId" to expressionOfInterest.jobId, diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt index 1cc579439..e2b30a130 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -79,7 +79,7 @@ class PutExpressionInterestServiceTest : val expectedMessage = HmppsMessage( messageId = "1", - eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED.name, + eventType = HmppsMessageEventType.ExpressionOfInterestCreated.name, messageAttributes = mapOf( "jobId" to "12345", @@ -95,7 +95,7 @@ class PutExpressionInterestServiceTest : assert(deserializedMap.containsKey("messageAttributes")) assert(deserializedMap.containsKey("eventType")) assertEquals( - expected = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED.name, + expected = "ExpressionOfInterestCreated", actual = eventType, ) @@ -104,6 +104,41 @@ class PutExpressionInterestServiceTest : messageAttributes?.containsKey("prisonNumber")?.let { assert(it) } } + it("should serialize ExpressionOfInterestMessage with ExpressionOfInterestCreated type") { + val expressionOfInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") + val expectedMessage = + HmppsMessage( + messageId = "1", + eventType = HmppsMessageEventType.ExpressionOfInterestCreated.name, + messageAttributes = + mapOf( + "jobId" to "12345", + "prisonNumber" to "H1234", + ), + ) + + val expectedMessageBody = objectMapper.writeValueAsString(expectedMessage) + val deserializedMap: Map = objectMapper.readValue(expectedMessageBody) + val eventType = deserializedMap["eventType"] + + assertEquals( + expected = "ExpressionOfInterestCreated", + actual = eventType, + ) + + whenever(mockObjectMapper.writeValueAsString(any())) + .thenReturn(expectedMessageBody) + + service.sendExpressionOfInterest(expressionOfInterest) + + verify(mockSqsClient).sendMessage( + argThat { request -> + request?.queueUrl() == "https://test-queue-url" && + objectMapper.readTree(request?.messageBody()) == objectMapper.readTree(expectedMessageBody) + }, + ) + } + it("allows messages of any type and anybody to be sent") { val externalType = "externalType" From 3441c0304b55498aff479b62b0931a6bff7780e4 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Mon, 10 Feb 2025 13:48:00 +0000 Subject: [PATCH 17/21] convention edits to enum --- .../models/hmpps/HmppsMessage.kt | 2 +- .../models/hmpps/HmppsMessageEventType.kt | 12 +++++- .../services/PutExpressionInterestService.kt | 2 +- .../PutExpressionInterestServiceTest.kt | 37 ++----------------- 4 files changed, 16 insertions(+), 37 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt index 1542f2712..6bc470065 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt @@ -2,7 +2,7 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps data class HmppsMessage( val messageId: String, - val eventType: String, + val eventType: HmppsMessageEventType, val description: String? = null, val messageAttributes: Map = emptyMap(), ) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt index 664d7deee..800bdd432 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt @@ -1,5 +1,13 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps -enum class HmppsMessageEventType { - ExpressionOfInterestCreated, +enum class HmppsMessageEventType( + val type: String, + val eventTypeCoe: String, + val description: String, +) { + EXPRESSION_OF_INTEREST_CREATED( + type = "mjma-jobs-board.job.created", + eventTypeCoe = "ExpressionOfInterestCreated", + description = "An expression of interest has been created", + ), } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt index 3f5dec43f..47408e150 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -27,7 +27,7 @@ class PutExpressionInterestService( objectMapper.writeValueAsString( HmppsMessage( messageId = UUID.randomUUID().toString(), - eventType = HmppsMessageEventType.ExpressionOfInterestCreated.name, + eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED, messageAttributes = mapOf( "jobId" to expressionOfInterest.jobId, diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt index e2b30a130..45bee66c3 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -79,7 +79,7 @@ class PutExpressionInterestServiceTest : val expectedMessage = HmppsMessage( messageId = "1", - eventType = HmppsMessageEventType.ExpressionOfInterestCreated.name, + eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED, messageAttributes = mapOf( "jobId" to "12345", @@ -95,7 +95,7 @@ class PutExpressionInterestServiceTest : assert(deserializedMap.containsKey("messageAttributes")) assert(deserializedMap.containsKey("eventType")) assertEquals( - expected = "ExpressionOfInterestCreated", + expected = "EXPRESSION_OF_INTEREST_CREATED", actual = eventType, ) @@ -109,7 +109,7 @@ class PutExpressionInterestServiceTest : val expectedMessage = HmppsMessage( messageId = "1", - eventType = HmppsMessageEventType.ExpressionOfInterestCreated.name, + eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED, messageAttributes = mapOf( "jobId" to "12345", @@ -122,7 +122,7 @@ class PutExpressionInterestServiceTest : val eventType = deserializedMap["eventType"] assertEquals( - expected = "ExpressionOfInterestCreated", + expected = "EXPRESSION_OF_INTEREST_CREATED", actual = eventType, ) @@ -138,34 +138,5 @@ class PutExpressionInterestServiceTest : }, ) } - - it("allows messages of any type and anybody to be sent") { - val externalType = "externalType" - - val expectedMessage = - HmppsMessage( - messageId = "1", - eventType = externalType, - messageAttributes = - mapOf( - "personId" to "12345", - "entityNumber" to "H1234", - ), - ) - - val serializedJson = objectMapper.writeValueAsString(expectedMessage) - - val deserializedMap: Map = objectMapper.readValue(serializedJson) - val eventType = deserializedMap["eventType"] - - assertEquals( - expected = externalType, - actual = eventType, - ) - - val messageAttributes = deserializedMap["messageAttributes"] as? Map - messageAttributes?.containsKey("personId")?.let { assert(it) } - messageAttributes?.containsKey("entityNumber")?.let { assert(it) } - } } }) From c20106c7a302e85ddeb4888a406460f4330a1ea1 Mon Sep 17 00:00:00 2001 From: rickchoijd Date: Mon, 10 Feb 2025 14:30:10 +0000 Subject: [PATCH 18/21] [ESWE-1080] revise enum naming - fix type at `eventTypeCoe` (to `eventTypeCode` - use `eventTypeCode` on message body/payload, instead of enum name - use `type` on message attribute - revise relevant tests --- .../models/hmpps/HmppsMessageEventType.kt | 8 +++++--- .../services/PutExpressionInterestService.kt | 5 +++-- .../services/PutExpressionInterestServiceTest.kt | 10 +++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt index 800bdd432..18fb7809b 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt @@ -1,13 +1,15 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps +import com.fasterxml.jackson.annotation.JsonValue + enum class HmppsMessageEventType( val type: String, - val eventTypeCoe: String, + @JsonValue val eventTypeCode: String, val description: String, ) { EXPRESSION_OF_INTEREST_CREATED( - type = "mjma-jobs-board.job.created", - eventTypeCoe = "ExpressionOfInterestCreated", + type = "mjma-jobs-board.job.expression-of-interest.created", + eventTypeCode = "ExpressionOfInterestCreated", description = "An expression of interest has been created", ), } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt index 47408e150..5f10d18bc 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -22,12 +22,13 @@ class PutExpressionInterestService( private val eoiQueueUrl by lazy { eoiQueue.queueUrl } fun sendExpressionOfInterest(expressionOfInterest: ExpressionOfInterest) { + val eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED try { val hmppsMessage = objectMapper.writeValueAsString( HmppsMessage( messageId = UUID.randomUUID().toString(), - eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED, + eventType = eventType, messageAttributes = mapOf( "jobId" to expressionOfInterest.jobId, @@ -41,7 +42,7 @@ class PutExpressionInterestService( .builder() .queueUrl(eoiQueueUrl) .messageBody(hmppsMessage) - .eventTypeMessageAttributes("mjma-jobs-board.job.expression-of-interest.created") + .eventTypeMessageAttributes(eventType.type) .build(), ) } catch (e: Exception) { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt index 45bee66c3..20d4eeb4c 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -46,7 +46,7 @@ class PutExpressionInterestServiceTest : describe("sendExpressionOfInterest") { it("should send a valid message successfully to SQS") { val expressionOfInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") - val messageBody = """{"messageId":"1","eventType":"EXPRESSION_OF_INTEREST_CREATED","messageAttributes":{"jobId":"12345","prisonNumber":"H1234"}}""" + val messageBody = """{"messageId":"1","eventType":"ExpressionOfInterestCreated","messageAttributes":{"jobId":"12345","prisonNumber":"H1234"}}""" whenever(mockObjectMapper.writeValueAsString(any())) .thenReturn(messageBody) @@ -95,11 +95,11 @@ class PutExpressionInterestServiceTest : assert(deserializedMap.containsKey("messageAttributes")) assert(deserializedMap.containsKey("eventType")) assertEquals( - expected = "EXPRESSION_OF_INTEREST_CREATED", + expected = "ExpressionOfInterestCreated", actual = eventType, ) - val messageAttributes = deserializedMap["messageAttributes"] as? Map + val messageAttributes = deserializedMap["messageAttributes"] as? Map<*, *> messageAttributes?.containsKey("jobId")?.let { assert(it) } messageAttributes?.containsKey("prisonNumber")?.let { assert(it) } } @@ -122,7 +122,7 @@ class PutExpressionInterestServiceTest : val eventType = deserializedMap["eventType"] assertEquals( - expected = "EXPRESSION_OF_INTEREST_CREATED", + expected = "ExpressionOfInterestCreated", actual = eventType, ) @@ -134,7 +134,7 @@ class PutExpressionInterestServiceTest : verify(mockSqsClient).sendMessage( argThat { request -> request?.queueUrl() == "https://test-queue-url" && - objectMapper.readTree(request?.messageBody()) == objectMapper.readTree(expectedMessageBody) + objectMapper.readTree(request.messageBody()) == objectMapper.readTree(expectedMessageBody) }, ) } From a696ded79f95f1dc133b1b05b7680477d745f053 Mon Sep 17 00:00:00 2001 From: rickchoijd Date: Mon, 10 Feb 2025 14:30:48 +0000 Subject: [PATCH 19/21] [ESWE-1080] revise error messages --- .../controllers/v1/person/ExpressionInterestController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index b42aa06f6..566138c18 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -48,12 +48,12 @@ class ExpressionInterestController( if (hmppsIdCheck.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { logger.info("ExpressionInterestController: Could not find nomis number for hmppsId: $hmppsId") - throw EntityNotFoundException("") + throw EntityNotFoundException("Could not find person with id: $hmppsId") } if (hmppsIdCheck.hasError(UpstreamApiError.Type.BAD_REQUEST)) { logger.info("ExpressionInterestController: Invalid hmppsId: $hmppsId") - throw ValidationException("") + throw ValidationException("Invalid HMPPS ID: $hmppsId") } val verifiedNomisNumber = getVerifiedNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() From 156f93fbcbc5be95adee426448125a03816f0e53 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Tue, 11 Feb 2025 12:36:50 +0000 Subject: [PATCH 20/21] refactor: move login in controller to EOI service --- .../v1/person/ExpressionInterestController.kt | 29 +-------- .../services/PutExpressionInterestService.kt | 36 +++++++++-- .../ExpressionInterestControllerTest.kt | 58 +++++------------ .../PutExpressionInterestServiceTest.kt | 63 ++++++++++++++++--- 4 files changed, 100 insertions(+), 86 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 566138c18..879503670 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -4,32 +4,20 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag -import jakarta.validation.ValidationException -import org.slf4j.Logger -import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.PutExpressionInterestService @RestController @RequestMapping("/v1/persons") @Tag(name = "persons") class ExpressionInterestController( - @Autowired val getPersonService: GetPersonService, @Autowired val putExpressionInterestService: PutExpressionInterestService, ) { - val logger: Logger = LoggerFactory.getLogger(this::class.java) - @PutMapping("{hmppsId}/expression-of-interest/jobs/{jobid}") @Operation( summary = "Returns completed response", @@ -44,23 +32,8 @@ class ExpressionInterestController( @Parameter(description = "A URL-encoded HMPPS identifier", example = "2008%2F0545166T") @PathVariable hmppsId: String, @Parameter(description = "A job identifier") @PathVariable jobid: String, ): ResponseEntity { - val hmppsIdCheck = getPersonService.getNomisNumber(hmppsId) - - if (hmppsIdCheck.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { - logger.info("ExpressionInterestController: Could not find nomis number for hmppsId: $hmppsId") - throw EntityNotFoundException("Could not find person with id: $hmppsId") - } - - if (hmppsIdCheck.hasError(UpstreamApiError.Type.BAD_REQUEST)) { - logger.info("ExpressionInterestController: Invalid hmppsId: $hmppsId") - throw ValidationException("Invalid HMPPS ID: $hmppsId") - } - - val verifiedNomisNumber = getVerifiedNomisNumber(hmppsIdCheck) ?: return ResponseEntity.badRequest().build() - putExpressionInterestService.sendExpressionOfInterest(ExpressionOfInterest(jobid, verifiedNomisNumber)) + putExpressionInterestService.sendExpressionOfInterest(hmppsId, jobid) return ResponseEntity.ok().build() } - - fun getVerifiedNomisNumber(nomisNumberResponse: Response) = nomisNumberResponse.data?.nomisNumber } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt index 5f10d18bc..4813cb1ca 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -1,12 +1,17 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.validation.ValidationException +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import software.amazon.awssdk.services.sqs.model.SendMessageRequest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsMessage import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsMessageEventType +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService import uk.gov.justice.hmpps.sqs.eventTypeMessageAttributes @@ -14,6 +19,7 @@ import java.util.UUID @Component class PutExpressionInterestService( + private val getPersonService: GetPersonService, private val hmppsQueueService: HmppsQueueService, private val objectMapper: ObjectMapper, ) { @@ -21,7 +27,29 @@ class PutExpressionInterestService( private val eoiQueueSqsClient by lazy { eoiQueue.sqsClient } private val eoiQueueUrl by lazy { eoiQueue.queueUrl } - fun sendExpressionOfInterest(expressionOfInterest: ExpressionOfInterest) { + companion object { + private val logger: Logger = LoggerFactory.getLogger(this::class.java) + } + + fun sendExpressionOfInterest( + hmppsId: String, + jobid: String, + ) { + val personResponse = getPersonService.getNomisNumber(hmppsId = hmppsId) + + if (personResponse.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { + logger.debug("ExpressionOfInterest: Could not find nomis number for hmppsId: $hmppsId") + throw EntityNotFoundException("Could not find person with id: $hmppsId") + } + + if (personResponse.hasError(UpstreamApiError.Type.BAD_REQUEST)) { + logger.debug("ExpressionOfInterest: Invalid hmppsId: $hmppsId") + throw ValidationException("Invalid HMPPS ID: $hmppsId") + } + + val nomisNumber = personResponse.data?.nomisNumber ?: run { throw ValidationException("Invalid HMPPS ID: $hmppsId") } + val expressionOfInterest = ExpressionOfInterest(jobid, nomisNumber) + val eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED try { val hmppsMessage = @@ -29,11 +57,7 @@ class PutExpressionInterestService( HmppsMessage( messageId = UUID.randomUUID().toString(), eventType = eventType, - messageAttributes = - mapOf( - "jobId" to expressionOfInterest.jobId, - "prisonNumber" to expressionOfInterest.prisonNumber, - ), + messageAttributes = with(expressionOfInterest) { mapOf("jobId" to jobId, "prisonNumber" to prisonNumber) }, ), ) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt index 6c2f8f9cb..6d655bb77 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -2,7 +2,9 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe -import org.mockito.kotlin.verify +import jakarta.validation.ValidationException +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest @@ -10,84 +12,52 @@ import org.springframework.http.HttpStatus import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.servlet.MockMvc +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.PutExpressionInterestService @WebMvcTest(controllers = [ExpressionInterestController::class]) @ActiveProfiles("test") class ExpressionInterestControllerTest( @Autowired var springMockMvc: MockMvc, - @MockitoBean val getPersonService: GetPersonService, @MockitoBean val expressionOfInterestService: PutExpressionInterestService, ) : DescribeSpec({ val mockMvc = IntegrationAPIMockMvc(springMockMvc) val basePath = "/v1/persons" val validHmppsId = "AABCD1ABC" - val nomisId = "AABCD1ABC" val invalidHmppsId = "INVALID_ID" val jobId = "5678" describe("PUT $basePath/{hmppsId}/expression-of-interest/jobs/{jobId}") { it("should return 404 Not Found if ENTITY_NOT_FOUND error occurs") { - val notFoundResponse = - Response( - data = null, - errors = - listOf( - UpstreamApiError( - type = UpstreamApiError.Type.ENTITY_NOT_FOUND, - description = "Entity not found", - causedBy = UpstreamApi.PRISONER_OFFENDER_SEARCH, - ), - ), - ) - whenever(getPersonService.getNomisNumber(validHmppsId)).thenReturn(notFoundResponse) + validHmppsId.let { id -> + whenever(expressionOfInterestService.sendExpressionOfInterest(id, jobId)).thenThrow(EntityNotFoundException("Could not find person with id: $id")) + } val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") result.response.status.shouldBe(HttpStatus.NOT_FOUND.value()) } it("should throw ValidationException if an invalid hmppsId is provided") { - val invalidResponse = - Response( - data = null, - errors = - listOf( - UpstreamApiError( - type = UpstreamApiError.Type.BAD_REQUEST, - description = "Invalid HmppsId", - causedBy = UpstreamApi.NOMIS, - ), - ), - ) - whenever(getPersonService.getNomisNumber(invalidHmppsId)).thenReturn(invalidResponse) + invalidHmppsId.let { id -> + whenever(expressionOfInterestService.sendExpressionOfInterest(id, jobId)).thenThrow(ValidationException("Invalid HMPPS ID: $id")) + } val result = mockMvc.performAuthorisedPut("$basePath/$invalidHmppsId/expression-of-interest/jobs/$jobId") result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value()) } it("should return 200 OK on successful expression of interest submission") { - val validNomisResponse = - Response( - data = NomisNumber(nomisId), - errors = emptyList(), - ) - whenever(getPersonService.getNomisNumber(validHmppsId)).thenReturn(validNomisResponse) + validHmppsId.let { id -> + doNothing().whenever(expressionOfInterestService).sendExpressionOfInterest(id, jobId) + } val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") - - verify(expressionOfInterestService).sendExpressionOfInterest(ExpressionOfInterest(jobId, nomisId)) result.response.status.shouldBe(HttpStatus.OK.value()) } it("should return 500 Server Error if an exception occurs") { - whenever(getPersonService.getNomisNumber(validHmppsId)).thenThrow(RuntimeException("Unexpected error")) + whenever(expressionOfInterestService.sendExpressionOfInterest(any(), any())).thenThrow(RuntimeException("Unexpected error")) val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") result.response.status.shouldBe(HttpStatus.INTERNAL_SERVER_ERROR.value()) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt index 20d4eeb4c..e610878b5 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe +import jakarta.validation.ValidationException import org.mockito.kotlin.any import org.mockito.kotlin.argThat import org.mockito.kotlin.doReturn @@ -14,17 +15,22 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import software.amazon.awssdk.services.sqs.SqsAsyncClient import software.amazon.awssdk.services.sqs.model.SendMessageRequest +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.MockMvcExtensions.objectMapper -import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ExpressionOfInterest import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsMessage import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsMessageEventType +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError import uk.gov.justice.hmpps.sqs.HmppsQueue import uk.gov.justice.hmpps.sqs.HmppsQueueService import kotlin.test.assertEquals class PutExpressionInterestServiceTest : DescribeSpec({ + val mockGetPersonService = mock() val mockQueueService = mock() val mockObjectMapper = mock() val mockSqsClient = mock() @@ -36,7 +42,7 @@ class PutExpressionInterestServiceTest : } val queId = "jobsboardintegration" - val service = PutExpressionInterestService(mockQueueService, mockObjectMapper) + val service = PutExpressionInterestService(mockGetPersonService, mockQueueService, mockObjectMapper) beforeTest { reset(mockQueueService, mockSqsClient, mockObjectMapper) @@ -44,14 +50,19 @@ class PutExpressionInterestServiceTest : } describe("sendExpressionOfInterest") { + beforeTest { + "H1234".let { whenever(mockGetPersonService.getNomisNumber(it)).thenReturn(Response(NomisNumber(it))) } + } + it("should send a valid message successfully to SQS") { - val expressionOfInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") + val jobId = "12345" + val hmppsId = "H1234" val messageBody = """{"messageId":"1","eventType":"ExpressionOfInterestCreated","messageAttributes":{"jobId":"12345","prisonNumber":"H1234"}}""" whenever(mockObjectMapper.writeValueAsString(any())) .thenReturn(messageBody) - service.sendExpressionOfInterest(expressionOfInterest) + service.sendExpressionOfInterest(hmppsId, jobId) verify(mockSqsClient).sendMessage( argThat { request: SendMessageRequest? -> @@ -62,14 +73,15 @@ class PutExpressionInterestServiceTest : } it("should throw MessageFailedException when SQS fails") { - val expressionInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") + val jobId = "12345" + val hmppsId = "H1234" whenever(mockSqsClient.sendMessage(any())) .thenThrow(RuntimeException("Failed to send message to SQS")) val exception = shouldThrow { - service.sendExpressionOfInterest(expressionInterest) + service.sendExpressionOfInterest(hmppsId, jobId) } exception.message shouldBe "Failed to send message to SQS" @@ -105,7 +117,8 @@ class PutExpressionInterestServiceTest : } it("should serialize ExpressionOfInterestMessage with ExpressionOfInterestCreated type") { - val expressionOfInterest = ExpressionOfInterest(jobId = "12345", prisonNumber = "H1234") + val jobId = "12345" + val hmppsId = "H1234" val expectedMessage = HmppsMessage( messageId = "1", @@ -129,7 +142,7 @@ class PutExpressionInterestServiceTest : whenever(mockObjectMapper.writeValueAsString(any())) .thenReturn(expectedMessageBody) - service.sendExpressionOfInterest(expressionOfInterest) + service.sendExpressionOfInterest(hmppsId, jobId) verify(mockSqsClient).sendMessage( argThat { request -> @@ -139,4 +152,38 @@ class PutExpressionInterestServiceTest : ) } } + + describe("sendExpressionOfInterest, with errors at HMPPS ID translation") { + val validHmppsId = "AABCD1ABC" + val invalidHmppsId = "INVALID_ID" + val jobId = "5678" + + it("should throw EntityNotFoundException. if ENTITY_NOT_FOUND error occurs") { + val hmppsId = validHmppsId + val notFoundResponse = errorResponseNomisNumber(UpstreamApiError.Type.ENTITY_NOT_FOUND, "Entity not found") + whenever(mockGetPersonService.getNomisNumber(hmppsId)).thenReturn(notFoundResponse) + + val exception = shouldThrow { service.sendExpressionOfInterest(hmppsId, jobId) } + + assertEquals("Could not find person with id: $hmppsId", exception.message) + } + + it("should throw ValidationException if an invalid hmppsId is provided") { + val hmppsId = invalidHmppsId + val invalidIdBadRequestResponse = errorResponseNomisNumber(UpstreamApiError.Type.BAD_REQUEST, "Invalid HMPPS ID") + whenever(mockGetPersonService.getNomisNumber(hmppsId)).thenReturn(invalidIdBadRequestResponse) + + val exception = shouldThrow { service.sendExpressionOfInterest(hmppsId, jobId) } + + assertEquals("Invalid HMPPS ID: $hmppsId", exception.message) + } + } }) + +private fun errorResponseNomisNumber( + errorType: UpstreamApiError.Type, + errorDescription: String, +) = Response( + data = null, + errors = listOf(UpstreamApiError(type = errorType, description = errorDescription, causedBy = UpstreamApi.PRISONER_OFFENDER_SEARCH)), +) From 4e67b5f71eff4f8d195a5c19ea54735081755e50 Mon Sep 17 00:00:00 2001 From: "gorton.brown" Date: Tue, 11 Feb 2025 16:28:06 +0000 Subject: [PATCH 21/21] API reponse refactor --- .../v1/person/ExpressionInterestController.kt | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt index 879503670..8dff7ee7f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -2,14 +2,16 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.PutExpressionInterestService @RestController @@ -22,18 +24,33 @@ class ExpressionInterestController( @Operation( summary = "Returns completed response", responses = [ - ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully submitted an expression of interest"), - ApiResponse(responseCode = "403", useReturnTypeSchema = true, description = "Access is forbidden"), - ApiResponse(responseCode = "400", useReturnTypeSchema = true, description = "Bad Request"), - ApiResponse(responseCode = "404", useReturnTypeSchema = true, description = "Not found"), + ApiResponse( + responseCode = "200", + useReturnTypeSchema = true, + description = "Successfully submitted an expression of interest", + ), + ApiResponse( + responseCode = "400", + description = "Bad Request", + content = [Content(schema = Schema(ref = "#/components/schemas/BadRequest"))], + ), + ApiResponse( + responseCode = "403", + description = "Access is forbidden", + content = [Content(schema = Schema(ref = "#/components/schemas/ForbiddenResponse"))], + ), + ApiResponse( + responseCode = "404", + content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))], + ), ], ) fun submitExpressionOfInterest( - @Parameter(description = "A URL-encoded HMPPS identifier", example = "2008%2F0545166T") @PathVariable hmppsId: String, + @Parameter(description = "A HMPPS identifier", example = "A1234AA") @PathVariable hmppsId: String, @Parameter(description = "A job identifier") @PathVariable jobid: String, - ): ResponseEntity { + ): Response { putExpressionInterestService.sendExpressionOfInterest(hmppsId, jobid) - return ResponseEntity.ok().build() + return Response(data = Unit, errors = emptyList()) } }