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..8dff7ee7f --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestController.kt @@ -0,0 +1,56 @@ +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.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 +@RequestMapping("/v1/persons") +@Tag(name = "persons") +class ExpressionInterestController( + @Autowired val putExpressionInterestService: PutExpressionInterestService, +) { + @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 = "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 HMPPS identifier", example = "A1234AA") @PathVariable hmppsId: String, + @Parameter(description = "A job identifier") @PathVariable jobid: String, + ): Response { + putExpressionInterestService.sendExpressionOfInterest(hmppsId, jobid) + + return Response(data = Unit, errors = emptyList()) + } +} 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..2c1e75a2a --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/exception/MessageFailedException.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception + +class MessageFailedException( + msg: String, + cause: Throwable? = null, +) : RuntimeException(msg, cause) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterest.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterest.kt new file mode 100644 index 000000000..56ed39737 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/ExpressionOfInterest.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps + +data class ExpressionOfInterest( + val jobId: String, + val prisonNumber: String, +) 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..6bc470065 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessage.kt @@ -0,0 +1,8 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps + +data class HmppsMessage( + val messageId: 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 new file mode 100644 index 000000000..18fb7809b --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/HmppsMessageEventType.kt @@ -0,0 +1,15 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps + +import com.fasterxml.jackson.annotation.JsonValue + +enum class HmppsMessageEventType( + val type: String, + @JsonValue val eventTypeCode: String, + val description: String, +) { + EXPRESSION_OF_INTEREST_CREATED( + 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 new file mode 100644 index 000000000..4813cb1ca --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestService.kt @@ -0,0 +1,76 @@ +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 +import java.util.UUID + +@Component +class PutExpressionInterestService( + private val getPersonService: GetPersonService, + private val hmppsQueueService: HmppsQueueService, + private val objectMapper: ObjectMapper, +) { + private val eoiQueue by lazy { hmppsQueueService.findByQueueId("jobsboardintegration") as HmppsQueue } + private val eoiQueueSqsClient by lazy { eoiQueue.sqsClient } + private val eoiQueueUrl by lazy { eoiQueue.queueUrl } + + 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 = + objectMapper.writeValueAsString( + HmppsMessage( + messageId = UUID.randomUUID().toString(), + eventType = eventType, + messageAttributes = with(expressionOfInterest) { mapOf("jobId" to jobId, "prisonNumber" to prisonNumber) }, + ), + ) + + eoiQueueSqsClient.sendMessage( + SendMessageRequest + .builder() + .queueUrl(eoiQueueUrl) + .messageBody(hmppsMessage) + .eventTypeMessageAttributes(eventType.type) + .build(), + ) + } catch (e: Exception) { + throw MessageFailedException("Failed to send message to SQS", e) + } + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index cd5531594..03dc75c1f 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -148,6 +148,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 d916c10ca..81b6c633a 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" + jobsboardintegration: + queueName: "jobsboard-integration" authorisation: consumers: @@ -73,6 +75,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 f3a7723a6..0aefaeb41 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/[^/]*$" @@ -80,3 +81,5 @@ hmpps.sqs: queues: audit: queueName: "audit" + jobsboardintegration: + queueName: "jobsboard-integration" diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 3852ba93e..7c02476d9 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: @@ -53,6 +55,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 b11c09e10..151371996 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 new file mode 100644 index 000000000..6d655bb77 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/ExpressionInterestControllerTest.kt @@ -0,0 +1,66 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person + +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.doNothing +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.exception.EntityNotFoundException +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.PutExpressionInterestService + +@WebMvcTest(controllers = [ExpressionInterestController::class]) +@ActiveProfiles("test") +class ExpressionInterestControllerTest( + @Autowired var springMockMvc: MockMvc, + @MockitoBean val expressionOfInterestService: PutExpressionInterestService, +) : DescribeSpec({ + val mockMvc = IntegrationAPIMockMvc(springMockMvc) + val basePath = "/v1/persons" + val validHmppsId = "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") { + 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") { + 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") { + validHmppsId.let { id -> + doNothing().whenever(expressionOfInterestService).sendExpressionOfInterest(id, jobId) + } + + val result = mockMvc.performAuthorisedPut("$basePath/$validHmppsId/expression-of-interest/jobs/$jobId") + result.response.status.shouldBe(HttpStatus.OK.value()) + } + + it("should return 500 Server Error if an exception occurs") { + 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/helpers/IntegrationAPIMockMvc.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/helpers/IntegrationAPIMockMvc.kt index ef9c91410..69bd6fc4c 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 @@ -17,6 +17,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/PutExpressionInterestServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt new file mode 100644 index 000000000..e610878b5 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/PutExpressionInterestServiceTest.kt @@ -0,0 +1,189 @@ +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 +import jakarta.validation.ValidationException +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +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.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.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() + + val eoiQueue = + mock { + on { sqsClient } doReturn mockSqsClient + on { queueUrl } doReturn "https://test-queue-url" + } + + val queId = "jobsboardintegration" + val service = PutExpressionInterestService(mockGetPersonService, mockQueueService, mockObjectMapper) + + beforeTest { + reset(mockQueueService, mockSqsClient, mockObjectMapper) + whenever(mockQueueService.findByQueueId(queId)).thenReturn(eoiQueue) + } + + describe("sendExpressionOfInterest") { + beforeTest { + "H1234".let { whenever(mockGetPersonService.getNomisNumber(it)).thenReturn(Response(NomisNumber(it))) } + } + + it("should send a valid message successfully to SQS") { + 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(hmppsId, jobId) + + verify(mockSqsClient).sendMessage( + argThat { request: SendMessageRequest? -> + request?.queueUrl() == "https://test-queue-url" && + request.messageBody() == messageBody + }, + ) + } + + it("should throw MessageFailedException when SQS fails") { + val jobId = "12345" + val hmppsId = "H1234" + + whenever(mockSqsClient.sendMessage(any())) + .thenThrow(RuntimeException("Failed to send message to SQS")) + + val exception = + shouldThrow { + service.sendExpressionOfInterest(hmppsId, jobId) + } + + exception.message shouldBe "Failed to send message to SQS" + } + + it("should serialize ExpressionOfInterestMessage with correct keys") { + val expectedMessage = + HmppsMessage( + messageId = "1", + eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED, + 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("messageAttributes")) + assert(deserializedMap.containsKey("eventType")) + assertEquals( + expected = "ExpressionOfInterestCreated", + actual = eventType, + ) + + val messageAttributes = deserializedMap["messageAttributes"] as? Map<*, *> + messageAttributes?.containsKey("jobId")?.let { assert(it) } + messageAttributes?.containsKey("prisonNumber")?.let { assert(it) } + } + + it("should serialize ExpressionOfInterestMessage with ExpressionOfInterestCreated type") { + val jobId = "12345" + val hmppsId = "H1234" + val expectedMessage = + HmppsMessage( + messageId = "1", + eventType = HmppsMessageEventType.EXPRESSION_OF_INTEREST_CREATED, + 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(hmppsId, jobId) + + verify(mockSqsClient).sendMessage( + argThat { request -> + request?.queueUrl() == "https://test-queue-url" && + objectMapper.readTree(request.messageBody()) == objectMapper.readTree(expectedMessageBody) + }, + ) + } + } + + 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)), +)