Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thin Slice for Risk Categories #383

Merged
merged 15 commits into from
Mar 5, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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.extensions.decodeUrlCharacters
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetRiskCategoriesForPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService

@RestController
@RequestMapping("/v1/persons")
class RiskCategoriesController(
@Autowired val getRiskCategoriesForPersonService: GetRiskCategoriesForPersonService,
@Autowired val auditService: AuditService,
) {

@GetMapping("{encodedHmppsId}/risks/categories")
fun getPersonRiskCategories(
@PathVariable encodedHmppsId: String,

): Map<String, RiskCategory?> {
val hmppsId = encodedHmppsId.decodeUrlCharacters()
val response = getRiskCategoriesForPersonService.execute(hmppsId)

if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
throw EntityNotFoundException("Could not find person with id: $hmppsId")
}
auditService.createEvent("GET_PERSON_RISK_CATEGORIES", "Person risk categories with hmpps id: $hmppsId has been retrieved")
return mapOf("data" to response.data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Alert
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ImageMetadata
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Offence
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Sentence
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.SentenceAdjustment
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.SentenceKeyDates
Expand All @@ -19,6 +20,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisAddres
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisAlert
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisBooking
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisImageDetail
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisInmateDetail
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisOffenceHistoryDetail
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisOffenderSentence
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisSentence
Expand Down Expand Up @@ -215,11 +217,43 @@ class NomisGateway(@Value("\${services.prison-api.base-url}") baseUrl: String) {
}
}

fun getRiskCategoriesForPerson(id: String): Response<RiskCategory?> {
val result = webClient.request<NomisInmateDetail>(
HttpMethod.GET,
"/api/offenders/$id",
authenticationHeaderForCategories(),
UpstreamApi.NOMIS,
)

return when (result) {
is WebClientWrapperResponse.Success -> {
Response(data = result.data.toRiskCategory())
}

is WebClientWrapperResponse.Error -> {
Response(
data = RiskCategory(),
errors = result.errors,
)
}
}
}

private fun authenticationHeader(): Map<String, String> {
val token = hmppsAuthGateway.getClientToken("NOMIS")

return mapOf(
"Authorization" to "Bearer $token",
)
}

private fun authenticationHeaderForCategories(): Map<String, String> {
val token = hmppsAuthGateway.getClientToken("NOMIS")
val version = "1.0"

return mapOf(
"Authorization" to "Bearer $token",
"version" to version,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps

data class RiskAssessment(
val classificationCode: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps

data class RiskCategory(
val offenderNo: String? = null,
val assessments: List<RiskAssessment> = emptyList(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis

import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskAssessment

data class NomisAssessment(
val classificationCode: String? = null,
) {
fun toRiskAssessment() = RiskAssessment(
classificationCode = this.classificationCode,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis

import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory

data class NomisInmateDetail(
val offenderNo: String? = null,
val assessments: List<NomisAssessment> = emptyList(),
) {
fun toRiskCategory(): RiskCategory = RiskCategory(
offenderNo = this.offenderNo,
assessments = this.assessments.map { it.toRiskAssessment() },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.NomisGateway
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory

@Service
class GetRiskCategoriesForPersonService(
@Autowired val nomisGateway: NomisGateway,
@Autowired val getPersonService: GetPersonService,
) {
fun execute(hmppsId: String): Response<RiskCategory?> {
val personResponse = getPersonService.execute(hmppsId = hmppsId)
val nomisNumber = personResponse.data?.identifiers?.nomisNumber

var personRiskCategories: Response<RiskCategory?> = Response(data = RiskCategory())

if (nomisNumber != null) {
personRiskCategories = nomisGateway.getRiskCategoriesForPerson(id = nomisNumber)
}

return Response(
data = personRiskCategories.data,
errors = personResponse.errors + personRiskCategories.errors,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import org.mockito.Mockito
import org.mockito.internal.verification.VerificationModeFactory
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.boot.test.mock.mockito.MockBean
import org.springframework.http.HttpStatus
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.removeWhitespaceAndNewlines
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskAssessment
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory
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.GetRiskCategoriesForPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

@WebMvcTest(controllers = [RiskCategoriesController::class])
@ActiveProfiles("test")
internal class RiskCategoriesControllerTest(
@Autowired var springMockMvc: MockMvc,
@MockBean val getRiskCategoriesForPersonService: GetRiskCategoriesForPersonService,
@MockBean val auditService: AuditService,
) : DescribeSpec(
{
val hmppsId = "9999/11111A"
val encodedHmppsId = URLEncoder.encode(hmppsId, StandardCharsets.UTF_8)
val path = "/v1/persons/$encodedHmppsId/risks/categories"
val mockMvc = IntegrationAPIMockMvc(springMockMvc)

describe("GET $path") {
beforeTest {
Mockito.reset(getRiskCategoriesForPersonService)
whenever(getRiskCategoriesForPersonService.execute(hmppsId)).thenReturn(
Response(
data = RiskCategory(
offenderNo = "A1234AA",
assessments = listOf(RiskAssessment(classificationCode = "C")),
),
),
)

Mockito.reset(auditService)
}

it("returns a 200 OK status code") {
val result = mockMvc.performAuthorised(path)

result.response.status.shouldBe(HttpStatus.OK.value())
}

it("gets the risk categories for a person with the matching ID") {
mockMvc.performAuthorised(path)
verify(getRiskCategoriesForPersonService, VerificationModeFactory.times(1)).execute(hmppsId)
}

it("logs audit") {
mockMvc.performAuthorised(path)

verify(auditService, VerificationModeFactory.times(1)).createEvent("GET_PERSON_RISK_CATEGORIES", "Person risk categories with hmpps id: $hmppsId has been retrieved")
}

it("returns the risk categories for a person with the matching ID") {
val result = mockMvc.performAuthorised(path)

result.response.contentAsString.shouldContain(
"""
"data": {
"offenderNo": "A1234AA",
"assessments": [
{
"classificationCode": "C"
}
]
}
""".removeWhitespaceAndNewlines(),
)
}

it("returns a 404 NOT FOUND status code when person isn't found in the upstream API") {
whenever(getRiskCategoriesForPersonService.execute(hmppsId)).thenReturn(
Response(
data = RiskCategory(),
errors = listOf(
UpstreamApiError(
causedBy = UpstreamApi.NOMIS,
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
),
),
),
)

val result = mockMvc.performAuthorised(path)

result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
}
}
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.nomis

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import org.mockito.Mockito
import org.mockito.internal.verification.VerificationModeFactory
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.HttpStatus
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.removeWhitespaceAndNewlines
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.HmppsAuthGateway
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.NomisGateway
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.mockservers.HmppsAuthMockServer
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.mockservers.NomisApiMockServer
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError

@ActiveProfiles("test")
@ContextConfiguration(
initializers = [ConfigDataApplicationContextInitializer::class],
classes = [NomisGateway::class],
)
class GetRiskCategoriesForPersonTest(
@MockBean val hmppsAuthGateway: HmppsAuthGateway,
val nomisGateway: NomisGateway,
) :
DescribeSpec(
{
val nomisApiMockServer = NomisApiMockServer()
val offenderNo = "A7796DY"

beforeEach {
nomisApiMockServer.start()
nomisApiMockServer.stubGetRiskCategoriesForPerson(
offenderNo,
"""
{
"offenderNo": "A7796DY",
"assessments": [
{
"classificationCode": "C"
}
]
}
""".removeWhitespaceAndNewlines(),
)

Mockito.reset(hmppsAuthGateway)
whenever(hmppsAuthGateway.getClientToken("NOMIS")).thenReturn(HmppsAuthMockServer.TOKEN)
}

afterTest {
nomisApiMockServer.stop()
}

it("authenticates using HMPPS Auth with credentials") {
nomisGateway.getRiskCategoriesForPerson(offenderNo)

verify(hmppsAuthGateway, VerificationModeFactory.times(1)).getClientToken("NOMIS")
}

it("returns an error when 404 Not Found is returned because no person is found") {
nomisApiMockServer.stubGetRiskCategoriesForPerson(offenderNo, "", HttpStatus.NOT_FOUND)

val response = nomisGateway.getRiskCategoriesForPerson(offenderNo)

response.errors.shouldHaveSize(1)
response.errors.first().causedBy.shouldBe(UpstreamApi.NOMIS)
response.errors.first().type.shouldBe(UpstreamApiError.Type.ENTITY_NOT_FOUND)
}
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,6 @@ class NomisApiMockServer : WireMockServer(WIREMOCK_PORT) {
private const val WIREMOCK_PORT = 4000
}

fun stubGetOffender(offenderNo: String, body: String, status: HttpStatus = HttpStatus.OK) {
stubFor(
get("/api/offenders/$offenderNo")
.withHeader(
"Authorization",
matching("Bearer ${HmppsAuthMockServer.TOKEN}"),
).willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
.withStatus(status.value())
.withBody(body.trimIndent()),
),
)
}

fun stubGetOffenderImageDetails(offenderNo: String, body: String, status: HttpStatus = HttpStatus.OK) {
stubFor(
get("/api/images/offenders/$offenderNo")
Expand Down Expand Up @@ -160,4 +145,18 @@ class NomisApiMockServer : WireMockServer(WIREMOCK_PORT) {
),
)
}

fun stubGetRiskCategoriesForPerson(offenderNo: String, body: String, status: HttpStatus = HttpStatus.OK) {
stubFor(
get("/api/offenders/$offenderNo")
.withHeader("Authorization", matching("Bearer ${HmppsAuthMockServer.TOKEN}"))
.withHeader("version", matching("1.0"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
.withStatus(status.value())
.withBody(body.trimIndent()),
),
)
}
}
Loading
Loading