diff --git a/build.gradle.kts b/build.gradle.kts index dbd5815..f2a1d11 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { testImplementation("uk.gov.justice.service.hmpps:hmpps-kotlin-spring-boot-starter-test:1.1.1") testImplementation("org.testcontainers:junit-jupiter:1.20.4") testImplementation("org.testcontainers:postgresql:1.20.4") + testImplementation("io.mockk:mockk:1.13.13") testImplementation("org.wiremock:wiremock-standalone:3.10.0") testImplementation("io.swagger.parser.v3:swagger-parser:2.1.24") { exclude(group = "io.swagger.core.v3") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishShoeSize.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishShoeSize.kt deleted file mode 100644 index dd8ab90..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishShoeSize.kt +++ /dev/null @@ -1,17 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedication.annotation - -import jakarta.validation.Constraint -import jakarta.validation.Payload -import uk.gov.justice.digital.hmpps.healthandmedication.validator.NullishShoeSizeValidator -import kotlin.reflect.KClass - -@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) -@Retention(AnnotationRetention.RUNTIME) -@Constraint(validatedBy = [NullishShoeSizeValidator::class]) -annotation class NullishShoeSize( - val min: String, - val max: String, - val message: String = "The value must be a whole or half shoe size within the specified range, null or Undefined.", - val groups: Array> = [], - val payload: Array> = [], -) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClientTrackingConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClientTrackingConfiguration.kt new file mode 100644 index 0000000..112eaea --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClientTrackingConfiguration.kt @@ -0,0 +1,66 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.config + +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import io.opentelemetry.api.trace.Span +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpHeaders +import org.springframework.web.servlet.HandlerInterceptor +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import java.text.ParseException + +@Configuration +@ConditionalOnExpression("T(org.apache.commons.lang3.StringUtils).isNotBlank('\${applicationinsights.connection.string:}')") +class ClientTrackingConfiguration(private val clientTrackingInterceptor: ClientTrackingInterceptor) : WebMvcConfigurer { + override fun addInterceptors(registry: InterceptorRegistry) { + log.info("Adding application insights client tracking interceptor") + registry.addInterceptor(clientTrackingInterceptor).addPathPatterns("/**") + } + + private companion object { + private val log = LoggerFactory.getLogger(this::class.java) + } +} + +@Configuration +class ClientTrackingInterceptor : HandlerInterceptor { + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { + val token = request.getHeader(HttpHeaders.AUTHORIZATION) + if (token?.startsWith(prefix = "Bearer ", ignoreCase = true) == true) { + try { + val jwtBody = getClaimsFromJWT(token) + val user = jwtBody.getClaim("user_name")?.toString() + val client = jwtBody.getClaim("client_id")?.toString() + + with(getCurrentSpan()) { + user?.run { + setAttribute("username", this) // username in customDimensions + setAttribute("enduser.id", this) // user_Id at the top level of the request + } + + client?.run { + setAttribute("clientId", this) + } + } + } catch (e: ParseException) { + log.warn("problem decoding jwt public key for application insights", e) + } + } + return true + } + + fun getCurrentSpan(): Span = Span.current() + + @Throws(ParseException::class) + private fun getClaimsFromJWT(token: String): JWTClaimsSet = + SignedJWT.parse(token.replace("Bearer ", "")).jwtClaimsSet + + private companion object { + private val log = LoggerFactory.getLogger(ClientTrackingInterceptor::class.java) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/OpenApiConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/OpenApiConfiguration.kt index 5e1c7dc..de20e47 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/OpenApiConfiguration.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/OpenApiConfiguration.kt @@ -14,7 +14,6 @@ import io.swagger.v3.oas.models.security.Scopes import io.swagger.v3.oas.models.security.SecurityRequirement import io.swagger.v3.oas.models.security.SecurityScheme import io.swagger.v3.oas.models.servers.Server -import io.swagger.v3.oas.models.tags.Tag import org.springdoc.core.customizers.OpenApiCustomizer import org.springframework.beans.factory.annotation.Value import org.springframework.boot.info.BuildProperties @@ -38,20 +37,10 @@ class OpenApiConfiguration( Server().url("http://localhost:8080").description("Local"), ), ) - .tags( - listOf( - // TODO: Remove the Popular and Examples tag and start adding your own tags to group your resources - Tag().name("Popular") - .description("The most popular endpoints. Look here first when deciding which endpoint to use."), - Tag().name("Examples").description("Endpoints for searching for a prisoner within a prison"), - ), - ) .info( Info().title("HMPPS Health And Medication Api").version(version) .contact(Contact().name("HMPPS Digital Studio").email("feedback@digital.justice.gov.uk")), ) - // TODO: Remove the default security schema and start adding your own schemas and roles to describe your - // service authorisation requirements .components( Components().addSecuritySchemes( "bearer-jwt", @@ -110,10 +99,3 @@ class OpenApiConfiguration( } } } - -private fun SecurityScheme.addBearerJwtRequirement(role: String): SecurityScheme = type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .`in`(SecurityScheme.In.HEADER) - .name("Authorization") - .description("A HMPPS Auth access token with the `$role` role.") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/HealthDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/HealthDto.kt index f1da86d..24be249 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/HealthDto.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/HealthDto.kt @@ -3,7 +3,7 @@ package uk.gov.justice.digital.hmpps.healthandmedication.dto.response import io.swagger.v3.oas.annotations.media.Schema import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataSimpleDto -@Schema(description = "Prison person health") +@Schema(description = "Health data") data class HealthDto( @Schema(description = "Smoker or vaper") val smokerOrVaper: ValueWithMetadata? = null, diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FoodAllergy.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FoodAllergy.kt index 2606e2c..2e3575b 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FoodAllergy.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FoodAllergy.kt @@ -39,6 +39,10 @@ class FoodAllergy( result = 31 * result + allergy.hashCode() return result } + + override fun toString(): String { + return "FoodAllergy(prisonerNumber='$prisonerNumber', allergy=$allergy, id=$id)" + } } data class FoodAllergies(val allergies: List) { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/MedicalDietaryRequirement.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/MedicalDietaryRequirement.kt index cc3cfab..93615a6 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/MedicalDietaryRequirement.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/MedicalDietaryRequirement.kt @@ -41,6 +41,10 @@ class MedicalDietaryRequirement( result = 31 * result + dietaryRequirement.hashCode() return result } + + override fun toString(): String { + return "MedicalDietaryRequirement(prisonerNumber='$prisonerNumber', dietaryRequirement=$dietaryRequirement, id=$id)" + } } data class MedicalDietaryRequirements(val medicalDietaryRequirements: List) { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/PrisonerHealth.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/PrisonerHealth.kt index 6b88f2a..afc321e 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/PrisonerHealth.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/PrisonerHealth.kt @@ -5,8 +5,6 @@ import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.FetchType.LAZY import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne import jakarta.persistence.MapKey import jakarta.persistence.OneToMany import jakarta.persistence.Table @@ -30,10 +28,6 @@ class PrisonerHealth( @Column(name = "prisoner_number", updatable = false, nullable = false) override val prisonerNumber: String, - @ManyToOne - @JoinColumn(name = "smoker_or_vaper", referencedColumnName = "id") - var smokerOrVaper: ReferenceDataCode? = null, - @OneToMany(mappedBy = "prisonerNumber", cascade = [ALL], orphanRemoval = true) var foodAllergies: MutableSet = mutableSetOf(), diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldHistoryRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldHistoryRepository.kt new file mode 100644 index 0000000..0ca494f --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldHistoryRepository.kt @@ -0,0 +1,14 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FieldHistory +import java.util.SortedSet + +@Repository +interface FieldHistoryRepository : JpaRepository { + fun findAllByPrisonerNumber(prisonerNumber: String): SortedSet + + fun findAllByPrisonerNumberAndField(prisonerNumber: String, field: HealthAndMedicationField): SortedSet +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldMetadataRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldMetadataRepository.kt new file mode 100644 index 0000000..371a2a7 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldMetadataRepository.kt @@ -0,0 +1,11 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FieldMetadata + +@Repository +interface FieldMetadataRepository : JpaRepository { + fun findAllByPrisonerNumber(prisonerNumber: String): List + fun deleteAllByPrisonerNumber(prisonerNumber: String) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishShoeSizeValidator.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishShoeSizeValidator.kt deleted file mode 100644 index 969666b..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishShoeSizeValidator.kt +++ /dev/null @@ -1,34 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedication.validator - -import jakarta.validation.ConstraintValidator -import jakarta.validation.ConstraintValidatorContext -import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishShoeSize -import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish -import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Defined -import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Undefined - -class NullishShoeSizeValidator : ConstraintValidator> { - - private var min: String = "0" - private var max: String = "0" - - override fun initialize(constraintAnnotation: NullishShoeSize) { - this.min = constraintAnnotation.min - this.max = constraintAnnotation.max - } - - override fun isValid(value: Nullish, context: ConstraintValidatorContext?): Boolean = when (value) { - Undefined -> true - is Defined -> value.value == null || isValidShoeSize(value.value) - } - - private fun isValidShoeSize(shoeSize: String): Boolean { - val validShowSize = """^([1-9]|1[0-9]|2[0-5])(\.5|\.0)?$""".toRegex() - if (!shoeSize.matches(validShowSize)) { - return false - } - - val number = shoeSize.toDouble() - return number in 1.0..25.0 - } -} diff --git a/src/main/resources/db/migration/common/V6__medical_dietary_requirement_data.sql b/src/main/resources/db/migration/common/V6__medical_dietary_requirement_data.sql index a33462b..a00f5bf 100644 --- a/src/main/resources/db/migration/common/V6__medical_dietary_requirement_data.sql +++ b/src/main/resources/db/migration/common/V6__medical_dietary_requirement_data.sql @@ -1,17 +1,16 @@ --- Seed data for tests INSERT INTO reference_data_domain (code, description, list_sequence, created_at, created_by) -VALUES ('MEDICAL_DIET', 'Medical diet', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'); +VALUES ('MEDICAL_DIET', 'Medical diet', 0, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'); INSERT INTO reference_data_code (id, domain, code, description, list_sequence, created_at, created_by) VALUES -('MEDICAL_DIET_COELIAC', 'MEDICAL_DIET', 'COELIAC', 'Coeliac (cannot eat gluten)', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_DIABETIC_TYPE_1', 'MEDICAL_DIET', 'DIABETIC_TYPE_1', 'Diabetic type 1', 1, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_DIABETIC_TYPE_2', 'MEDICAL_DIET', 'DIABETIC_TYPE_2', 'Diabetic type 2', 2, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_DYSPHAGIA', 'MEDICAL_DIET', 'DYSPHAGIA', 'Dysphagia (has problems swallowing food)', 3, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_EATING_DISORDER', 'MEDICAL_DIET', 'EATING_DISORDER', 'Eating disorder', 4, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_LACTOSE_INTOLERANT', 'MEDICAL_DIET', 'LACTOSE_INTOLERANT', 'Lactose intolerant', 5, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_LOW_CHOLESTEROL', 'MEDICAL_DIET', 'LOW_CHOLESTEROL', 'Low cholesterol', 6, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_LOW_PHOSPHOROUS', 'MEDICAL_DIET', 'LOW_PHOSPHOROUS', 'Low phosphorous diet', 7, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_NUTRIENT_DEFICIENCY', 'MEDICAL_DIET', 'NUTRIENT_DEFICIENCY', 'Nutrient deficiency', 7, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('MEDICAL_DIET_OTHER', 'MEDICAL_DIET', 'OTHER', 'Other', 8, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'); +('MEDICAL_DIET_COELIAC', 'MEDICAL_DIET', 'COELIAC', 'Coeliac (cannot eat gluten)', 0, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_DIABETIC_TYPE_1', 'MEDICAL_DIET', 'DIABETIC_TYPE_1', 'Diabetic type 1', 1, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_DIABETIC_TYPE_2', 'MEDICAL_DIET', 'DIABETIC_TYPE_2', 'Diabetic type 2', 2, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_DYSPHAGIA', 'MEDICAL_DIET', 'DYSPHAGIA', 'Dysphagia (has problems swallowing food)', 3, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_EATING_DISORDER', 'MEDICAL_DIET', 'EATING_DISORDER', 'Eating disorder', 4, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_LACTOSE_INTOLERANT', 'MEDICAL_DIET', 'LACTOSE_INTOLERANT', 'Lactose intolerant', 5, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_LOW_CHOLESTEROL', 'MEDICAL_DIET', 'LOW_CHOLESTEROL', 'Low cholesterol', 6, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_LOW_PHOSPHOROUS', 'MEDICAL_DIET', 'LOW_PHOSPHOROUS', 'Low phosphorous diet', 7, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_NUTRIENT_DEFICIENCY', 'MEDICAL_DIET', 'NUTRIENT_DEFICIENCY', 'Nutrient deficiency', 7, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('MEDICAL_DIET_OTHER', 'MEDICAL_DIET', 'OTHER', 'Other', 8, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'); diff --git a/src/main/resources/db/migration/common/V8__food_allergy_data.sql b/src/main/resources/db/migration/common/V8__food_allergy_data.sql index fbfbe90..340398a 100644 --- a/src/main/resources/db/migration/common/V8__food_allergy_data.sql +++ b/src/main/resources/db/migration/common/V8__food_allergy_data.sql @@ -1,22 +1,21 @@ --- Seed data for tests INSERT INTO reference_data_domain (code, description, list_sequence, created_at, created_by) -VALUES ('FOOD_ALLERGY', 'Food allergy', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'); +VALUES ('FOOD_ALLERGY', 'Food allergy', 0, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'); INSERT INTO reference_data_code (id, domain, code, description, list_sequence, created_at, created_by) VALUES -('FOOD_ALLERGY_CELERY', 'FOOD_ALLERGY', 'CELERY', 'Celery', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_GLUTEN', 'FOOD_ALLERGY', 'GLUTEN', 'Cereals containing gluten', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_CRUSTACEANS', 'FOOD_ALLERGY', 'CRUSTACEANS', 'Crustaceans', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_EGG', 'FOOD_ALLERGY', 'EGG', 'Egg', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_FISH', 'FOOD_ALLERGY', 'FISH', 'Fish', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_LUPIN', 'FOOD_ALLERGY', 'LUPIN', 'Lupin', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_MILK', 'FOOD_ALLERGY', 'MILK', 'Milk', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_MOLLUSCS', 'FOOD_ALLERGY', 'MOLLUSCS', 'Molluscs', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_MUSTARD', 'FOOD_ALLERGY', 'MUSTARD', 'Mustard', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_PEANUTS', 'FOOD_ALLERGY', 'PEANUTS', 'Peanuts', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_SESAME', 'FOOD_ALLERGY', 'SESAME', 'Sesame', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_SOYA', 'FOOD_ALLERGY', 'SOYA', 'Soya', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_SULPHUR_DIOXIDE', 'FOOD_ALLERGY', 'SULPHUR_DIOXIDE', 'Sulphur dioxide', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_TREE_NUTS', 'FOOD_ALLERGY', 'TREE_NUTS', 'Tree nuts', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), -('FOOD_ALLERGY_OTHER', 'FOOD_ALLERGY', 'OTHER', 'Other', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'); +('FOOD_ALLERGY_CELERY', 'FOOD_ALLERGY', 'CELERY', 'Celery', 0, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_GLUTEN', 'FOOD_ALLERGY', 'GLUTEN', 'Cereals containing gluten', 1, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_CRUSTACEANS', 'FOOD_ALLERGY', 'CRUSTACEANS', 'Crustaceans', 2, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_EGG', 'FOOD_ALLERGY', 'EGG', 'Egg', 3, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_FISH', 'FOOD_ALLERGY', 'FISH', 'Fish', 4, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_LUPIN', 'FOOD_ALLERGY', 'LUPIN', 'Lupin', 5, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_MILK', 'FOOD_ALLERGY', 'MILK', 'Milk', 6, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_MOLLUSCS', 'FOOD_ALLERGY', 'MOLLUSCS', 'Molluscs', 7, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_MUSTARD', 'FOOD_ALLERGY', 'MUSTARD', 'Mustard', 8, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_PEANUTS', 'FOOD_ALLERGY', 'PEANUTS', 'Peanuts', 9, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_SESAME', 'FOOD_ALLERGY', 'SESAME', 'Sesame', 10, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_SOYA', 'FOOD_ALLERGY', 'SOYA', 'Soya', 11, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_SULPHUR_DIOXIDE', 'FOOD_ALLERGY', 'SULPHUR_DIOXIDE', 'Sulphur dioxide', 12, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_TREE_NUTS', 'FOOD_ALLERGY', 'TREE_NUTS', 'Tree nuts', 13, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'), +('FOOD_ALLERGY_OTHER', 'FOOD_ALLERGY', 'OTHER', 'Other', 14, '2025-01-16 00:00:00+0000', 'CONNECT_DPS'); diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/PrisonerSearchClientTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/PrisonerSearchClientTest.kt new file mode 100644 index 0000000..62fccfe --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/PrisonerSearchClientTest.kt @@ -0,0 +1,72 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClientResponseException +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.dto.PrisonerDto +import uk.gov.justice.digital.hmpps.healthandmedication.config.DownstreamServiceException +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.PRISONER_NUMBER +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.PRISONER_NUMBER_NOT_FOUND +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.PRISONER_NUMBER_THROW_EXCEPTION +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.PRISON_ID +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.PrisonerSearchServer + +class PrisonerSearchClientTest { + private lateinit var client: PrisonerSearchClient + + @BeforeEach + fun resetMocks() { + server.resetRequests() + val webClient = WebClient.create("http://localhost:${server.port()}") + client = PrisonerSearchClient(webClient) + } + + @Test + fun `getPrisoner - success`() { + server.stubGetPrisoner() + + val result = client.getPrisoner(PRISONER_NUMBER) + + assertThat(result!!).isEqualTo(PrisonerDto(prisonerNumber = PRISONER_NUMBER, prisonId = PRISON_ID)) + } + + @Test + fun `getPrisoner - prisoner not found`() { + val result = client.getPrisoner(PRISONER_NUMBER_NOT_FOUND) + + assertThat(result).isNull() + } + + @Test + fun `getPrisoner - downstream service exception`() { + server.stubGetPrisonerException() + + assertThatThrownBy { client.getPrisoner(PRISONER_NUMBER_THROW_EXCEPTION) } + .isInstanceOf(DownstreamServiceException::class.java) + .hasMessage("Get prisoner request failed") + .hasCauseInstanceOf(WebClientResponseException::class.java) + .hasRootCauseMessage("500 Internal Server Error from GET http://localhost:8112/prisoner/${PRISONER_NUMBER_THROW_EXCEPTION}") + } + + companion object { + @JvmField + internal val server = PrisonerSearchServer() + + @BeforeAll + @JvmStatic + fun startMocks() { + server.start() + } + + @AfterAll + @JvmStatic + fun stopMocks() { + server.stop() + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClientTrackingConfigurationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClientTrackingConfigurationTest.kt new file mode 100644 index 0000000..d7c22a3 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClientTrackingConfigurationTest.kt @@ -0,0 +1,110 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.config + +import io.opentelemetry.api.trace.Span +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer +import org.springframework.context.annotation.Import +import org.springframework.http.HttpHeaders +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.junit.jupiter.SpringExtension + +@TestPropertySource( + properties = [ + "applicationinsights.connection.string=TEST", + ], +) +@Import(JwtAuthHelper::class, ClientTrackingInterceptor::class, ClientTrackingConfiguration::class) +@ContextConfiguration(initializers = [ConfigDataApplicationContextInitializer::class]) +@ActiveProfiles("test") +@ExtendWith(SpringExtension::class) +class ClientTrackingConfigurationTest { + @Suppress("SpringJavaInjectionPointsAutowiringInspection") + @Autowired + private lateinit var interceptor: ClientTrackingInterceptor + private lateinit var interceptorSpy: ClientTrackingInterceptor + + @Suppress("SpringJavaInjectionPointsAutowiringInspection") + @Autowired + private lateinit var jwtAuthHelper: JwtAuthHelper + + private val span = spy(Span.current()) + + private val res = MockHttpServletResponse() + private val req = MockHttpServletRequest() + + @BeforeEach + fun setUp() { + interceptorSpy = spy(interceptor) + whenever(interceptorSpy.getCurrentSpan()).thenReturn(span) + } + + @Test + fun `set user attributes only`() { + val user = "TEST_USER" + val token = jwtAuthHelper.createJwt(subject = user, user = user, client = null) + req.addHeader(HttpHeaders.AUTHORIZATION, "Bearer $token") + + interceptorSpy.preHandle(req, res, "null") + + verify(span).setAttribute("username", user) + verify(span).setAttribute("enduser.id", user) + verifyNoMoreInteractions(span) + } + + @Test + fun `set client id attribute only`() { + val token = jwtAuthHelper.createJwt(subject = CLIENT_ID, user = null, client = CLIENT_ID) + req.addHeader(HttpHeaders.AUTHORIZATION, "Bearer $token") + + interceptorSpy.preHandle(req, res, "null") + + verify(span).setAttribute("clientId", CLIENT_ID) + verifyNoMoreInteractions(span) + } + + @Test + fun `set user and client attributes`() { + val user = "TEST_USER" + val token = jwtAuthHelper.createJwt(subject = user, user = user, client = CLIENT_ID) + req.addHeader(HttpHeaders.AUTHORIZATION, "Bearer $token") + + interceptorSpy.preHandle(req, res, "null") + + verify(span).setAttribute("username", user) + verify(span).setAttribute("enduser.id", user) + verify(span).setAttribute("clientId", CLIENT_ID) + verifyNoMoreInteractions(span) + } + + @Test + fun `no bearer token causes no attributes to be set`() { + val token = jwtAuthHelper.createJwt(subject = CLIENT_ID, user = null, client = CLIENT_ID) + req.addHeader(HttpHeaders.AUTHORIZATION, "Not-Bearer $token") + + interceptorSpy.preHandle(req, res, "null") + + verifyNoInteractions(span) + } + + @Test + fun `jwt parse exception causes no attributes to be set`() { + val token = "invalid" + req.addHeader(HttpHeaders.AUTHORIZATION, "Bearer $token") + + interceptorSpy.preHandle(req, res, "null") + + verifyNoInteractions(span) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/JwtAuthHelper.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/JwtAuthHelper.kt new file mode 100644 index 0000000..b1ecb77 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/JwtAuthHelper.kt @@ -0,0 +1,83 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.config + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.Jwts.SIG +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder +import org.springframework.stereotype.Component +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.interfaces.RSAPublicKey +import java.time.Duration +import java.util.Date +import java.util.UUID + +internal const val CLIENT_ID = "prison-person-api-client" + +@Component +class JwtAuthHelper { + private lateinit var keyPair: KeyPair + + init { + val gen = KeyPairGenerator.getInstance("RSA") + gen.initialize(2048) + keyPair = gen.generateKeyPair() + } + + @Bean + fun jwtDecoder(): JwtDecoder = NimbusJwtDecoder.withPublicKey(keyPair.public as RSAPublicKey).build() + + fun setAuthorisation( + user: String? = null, + client: String = CLIENT_ID, + roles: List = listOf(), + scopes: List = listOf(), + isUserToken: Boolean = true, + ): (HttpHeaders) -> Unit { + val token = createJwt( + subject = user ?: client, + user = user, + client = client, + scope = scopes, + expiryTime = Duration.ofHours(1L), + roles = roles, + isUserToken = isUserToken, + ) + return { it.set(HttpHeaders.AUTHORIZATION, "Bearer $token") } + } + + internal fun createJwt( + subject: String, + user: String?, + client: String?, + scope: List? = listOf(), + roles: List? = listOf(), + expiryTime: Duration = Duration.ofHours(1), + jwtId: String = UUID.randomUUID().toString(), + isUserToken: Boolean = true, + ): String = + mutableMapOf() + .also { + user?.let { user -> + it[ + when (isUserToken) { + true -> "user_name" false -> "username" + }, + ] = user + } + } + .also { client?.let { client -> it["client_id"] = client } } + .also { roles?.let { roles -> it["authorities"] = roles } } + .also { scope?.let { scope -> it["scope"] = scope } } + .let { + Jwts.builder() + .id(jwtId) + .subject(subject) + .claims(it.toMap()) + .expiration(Date(System.currentTimeMillis() + expiryTime.toMillis())) + .signWith(keyPair.private, SIG.RS256) + .compact() + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/OpenApiDocsIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/OpenApiDocsIntTest.kt similarity index 95% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/OpenApiDocsIntTest.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/OpenApiDocsIntTest.kt index 1a50208..26b02f8 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/OpenApiDocsIntTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/OpenApiDocsIntTest.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.healthandmedication.integration +package uk.gov.justice.digital.hmpps.healthandmedication.controller import io.swagger.v3.parser.OpenAPIV3Parser import net.minidev.json.JSONArray @@ -9,6 +9,7 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDO import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.http.MediaType import org.springframework.test.context.ActiveProfiles +import uk.gov.justice.digital.hmpps.healthandmedication.integration.IntegrationTestBase import java.time.LocalDate import java.time.format.DateTimeFormatter.ISO_DATE diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/PrisonerPrisonerHealthControllerIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/PrisonerPrisonerHealthControllerIntTest.kt new file mode 100644 index 0000000..a65aedd --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/PrisonerPrisonerHealthControllerIntTest.kt @@ -0,0 +1,319 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.controller + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.test.context.jdbc.Sql +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField.FOOD_ALLERGY +import uk.gov.justice.digital.hmpps.healthandmedication.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FoodAllergies +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.JsonObject +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.utils.HistoryComparison +import java.time.Clock +import java.time.ZoneId +import java.time.ZonedDateTime + +class PrisonerPrisonerHealthControllerIntTest : IntegrationTestBase() { + + @TestConfiguration + class FixedClockConfig { + @Primary + @Bean + fun fixedClock(): Clock = clock + } + + @DisplayName("PATCH /prisoners/{prisonerNumber}") + @Nested + inner class SetPrisonerHealthTest { + + @Nested + inner class Security { + + @Test + fun `access forbidden when no authority`() { + webTestClient.patch().uri("/prisoners/${PRISONER_NUMBER}").header("Content-Type", "application/json") + .bodyValue(VALID_REQUEST_BODY).exchange().expectStatus().isUnauthorized + } + + @Test + fun `access forbidden when no role`() { + webTestClient.patch().uri("/prisoners/${PRISONER_NUMBER}").headers(setAuthorisation(roles = listOf())) + .header("Content-Type", "application/json").bodyValue(VALID_REQUEST_BODY).exchange() + .expectStatus().isForbidden + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.patch().uri("/prisoners/${PRISONER_NUMBER}") + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))).header("Content-Type", "application/json") + .bodyValue(VALID_REQUEST_BODY).exchange().expectStatus().isForbidden + } + } + + @Nested + @Sql("classpath:jpa/repository/reset.sql") + inner class Validation { + + @Test + fun `bad request when field type is not as expected`() { + expectBadRequestFrom( + prisonerNumber = PRISONER_NUMBER, + requestBody = """{ "foodAllergies": 123 }""", + message = "Validation failure: Couldn't read request body", + ) + } + + @Test + fun `bad request when prisoner not found`() { + expectBadRequestFrom( + prisonerNumber = "PRISONER_NOT_FOUND", + requestBody = VALID_REQUEST_BODY, + message = "Validation failure: Prisoner number 'PRISONER_NOT_FOUND' not found", + ) + } + + @Nested + inner class MedicalDietaryRequirements { + @Test + fun `bad request when null for medical dietary requirements`() { + expectBadRequestFrom( + PRISONER_NUMBER, + requestBody = """{ "medicalDietaryRequirements": null }""", + message = "Validation failure(s): The value must be a a list of domain codes of the correct domain, an empty list, or Undefined.", + ) + } + + @Test + fun `bad request when incorrect domain for medical dietary requirements`() { + expectBadRequestFrom( + PRISONER_NUMBER, + requestBody = """{ "medicalDietaryRequirements": ["MEDICAL_DIET_FREE_FROM","FOOD_ALLERGY_EGG"] }""".trimMargin(), + message = "Validation failure(s): The value must be a a list of domain codes of the correct domain, an empty list, or Undefined.", + ) + } + } + + @Nested + inner class FoodAllergies { + @Test + fun `bad request when null for medical dietary requirements`() { + expectBadRequestFrom( + PRISONER_NUMBER, + requestBody = """{ "foodAllergies": null }""", + message = "Validation failure(s): The value must be a a list of domain codes of the correct domain, an empty list, or Undefined.", + ) + } + + @Test + fun `bad request when incorrect domain for medical dietary requirements`() { + expectBadRequestFrom( + PRISONER_NUMBER, + requestBody = """{ "foodAllergies": ["MEDICAL_DIET_FREE_FROM","FOOD_ALLERGY_EGG"] }""".trimMargin(), + message = "Validation failure(s): The value must be a a list of domain codes of the correct domain, an empty list, or Undefined.", + ) + } + } + + private fun expectBadRequestFrom(prisonerNumber: String, requestBody: String, message: String) { + webTestClient.patch().uri("/prisoners/$prisonerNumber") + .headers(setAuthorisation(roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__HEALTH_AND_MEDICATION_DATA__RW"))) + .header("Content-Type", "application/json").bodyValue(requestBody).exchange() + .expectStatus().isBadRequest.expectBody().jsonPath("userMessage").isEqualTo(message) + } + } + + @Nested + inner class HappyPath { + + @Test + @Sql("classpath:jpa/repository/reset.sql") + fun `can create new health information`() { + expectSuccessfulUpdateFrom(VALID_REQUEST_BODY).expectBody().json( + MILK_ALLERGY_ALL_UPDATED_RESPONSE, + true, + ) + + expectFieldHistory( + FOOD_ALLERGY, + HistoryComparison( + value = JsonObject(FOOD_ALLERGY, FoodAllergies(listOf("FOOD_ALLERGY_MILK"))), + createdAt = NOW, + createdBy = USER1, + ), + ) + } + + @Test + @Sql("classpath:jpa/repository/reset.sql") + @Sql("classpath:resource/healthandmedication/health.sql") + @Sql("classpath:resource/healthandmedication/food_allergies.sql") + @Sql("classpath:resource/healthandmedication/medical_dietary_requirements.sql") + @Sql("classpath:resource/healthandmedication/field_metadata.sql") + @Sql("classpath:resource/healthandmedication/field_history.sql") + fun `can update existing health information`() { + expectFieldHistory( + FOOD_ALLERGY, + HistoryComparison( + value = JsonObject(FOOD_ALLERGY, FoodAllergies(listOf("FOOD_ALLERGY_SOYA"))), + createdAt = THEN, + createdBy = USER1, + ), + ) + + expectSuccessfulUpdateFrom(VALID_REQUEST_BODY).expectBody().json( + MILK_ALLERGY_MEDICAL_NOT_UPDATED_RESPONSE, + true, + ) + + expectFieldHistory( + FOOD_ALLERGY, + HistoryComparison( + value = JsonObject(FOOD_ALLERGY, FoodAllergies(listOf("FOOD_ALLERGY_SOYA"))), + createdAt = THEN, + createdBy = USER1, + ), + HistoryComparison( + value = JsonObject(FOOD_ALLERGY, FoodAllergies(listOf("FOOD_ALLERGY_MILK"))), + createdAt = NOW, + createdBy = USER1, + ), + ) + } + + @Test + @Sql("classpath:jpa/repository/reset.sql") + @Sql("classpath:resource/healthandmedication/food_allergies.sql") + @Sql("classpath:resource/healthandmedication/medical_dietary_requirements.sql") + @Sql("classpath:resource/healthandmedication/field_metadata.sql") + @Sql("classpath:resource/healthandmedication/field_history.sql") + fun `can update existing health information to empty list`() { + expectFieldHistory( + FOOD_ALLERGY, + HistoryComparison( + value = JsonObject(FOOD_ALLERGY, FoodAllergies(listOf("FOOD_ALLERGY_SOYA"))), + createdAt = THEN, + createdBy = USER1, + ), + ) + + expectSuccessfulUpdateFrom( + // language=json + """ + { "foodAllergies": [], "medicalDietaryRequirements": [] } + """.trimIndent(), + ).expectBody().json( + // language=json + """ + { "smokerOrVaper": null, + "foodAllergies": { + "value": [], + "lastModifiedAt":"2024-06-14T09:10:11+0100", + "lastModifiedBy":"USER1" + }, + "medicalDietaryRequirements": { + "value": [], + "lastModifiedAt":"2024-06-14T09:10:11+0100", + "lastModifiedBy":"USER1" + } + } + """.trimIndent(), + true, + ) + + expectFieldHistory( + FOOD_ALLERGY, + HistoryComparison( + value = JsonObject(FOOD_ALLERGY, FoodAllergies(listOf("FOOD_ALLERGY_SOYA"))), + createdAt = THEN, + createdBy = USER1, + ), + HistoryComparison( + value = JsonObject(FOOD_ALLERGY, FoodAllergies(emptyList())), + createdAt = NOW, + createdBy = USER1, + ), + ) + } + + private fun expectSuccessfulUpdateFrom(requestBody: String, user: String? = USER1) = + webTestClient.patch().uri("/prisoners/${PRISONER_NUMBER}") + .headers(setAuthorisation(user, roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__HEALTH_AND_MEDICATION_DATA__RW"))) + .header("Content-Type", "application/json").bodyValue(requestBody).exchange().expectStatus().isOk + } + } + + private companion object { + const val PRISONER_NUMBER = "A1234AA" + const val USER1 = "USER1" + val NOW = ZonedDateTime.now(clock) + val THEN = ZonedDateTime.of(2024, 1, 2, 9, 10, 11, 123000000, ZoneId.of("Europe/London")) + + val VALID_REQUEST_BODY = + // language=json + """ + { + "foodAllergies": ["FOOD_ALLERGY_MILK"] + } + """.trimIndent() + + val MILK_ALLERGY_ALL_UPDATED_RESPONSE = + // language=json + """ + { + "smokerOrVaper": null, + "foodAllergies": { + "value": [ + { + "id": "FOOD_ALLERGY_MILK", + "description": "Milk", + "listSequence": 6, + "isActive": true + } + ], + "lastModifiedAt": "2024-06-14T09:10:11+0100", + "lastModifiedBy": "USER1" + }, + "medicalDietaryRequirements": { + "value": [], + "lastModifiedAt": "2024-06-14T09:10:11+0100", + "lastModifiedBy": "USER1" + } + } + """.trimIndent() + + val MILK_ALLERGY_MEDICAL_NOT_UPDATED_RESPONSE = + // language=json + """ + { + "smokerOrVaper": null, + "foodAllergies": { + "value": [ + { + "id": "FOOD_ALLERGY_MILK", + "description": "Milk", + "listSequence": 6, + "isActive": true + } + ], + "lastModifiedAt": "2024-06-14T09:10:11+0100", + "lastModifiedBy": "USER1" + }, + "medicalDietaryRequirements": { + "value": [ + { + "id": "MEDICAL_DIET_LOW_CHOLESTEROL", + "description": "Low cholesterol", + "listSequence": 6, + "isActive": true + } + ], + "lastModifiedAt": "2024-01-02T09:10:11+0000", + "lastModifiedBy": "USER1" + } + } + """.trimIndent() + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/ReferenceDataCodeControllerIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/ReferenceDataCodeControllerIntTest.kt new file mode 100644 index 0000000..698176d --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/ReferenceDataCodeControllerIntTest.kt @@ -0,0 +1,263 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.controller + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.healthandmedication.integration.IntegrationTestBase + +class ReferenceDataCodeControllerIntTest : IntegrationTestBase() { + + @DisplayName("GET /reference-data/domains/{domain}/codes") + @Nested + inner class GetReferenceDataCodesTest { + + @Nested + inner class Security { + + @Test + fun `access forbidden when no authority`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY/codes") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY/codes") + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))) + .exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can retrieve reference data codes`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY/codes") + .headers(setAuthorisation(roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO"))) + .exchange() + .expectStatus().isOk + .expectBody().json( + """ + [ + { + "id": "FOOD_ALLERGY_CELERY", + "domain": "FOOD_ALLERGY", + "code": "CELERY", + "description": "Celery", + "listSequence": 0, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_GLUTEN", + "domain": "FOOD_ALLERGY", + "code": "GLUTEN", + "description": "Cereals containing gluten", + "listSequence": 1, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_CRUSTACEANS", + "domain": "FOOD_ALLERGY", + "code": "CRUSTACEANS", + "description": "Crustaceans", + "listSequence": 2, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_EGG", + "domain": "FOOD_ALLERGY", + "code": "EGG", + "description": "Egg", + "listSequence": 3, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_FISH", + "domain": "FOOD_ALLERGY", + "code": "FISH", + "description": "Fish", + "listSequence": 4, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_LUPIN", + "domain": "FOOD_ALLERGY", + "code": "LUPIN", + "description": "Lupin", + "listSequence": 5, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_MILK", + "domain": "FOOD_ALLERGY", + "code": "MILK", + "description": "Milk", + "listSequence": 6, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_MOLLUSCS", + "domain": "FOOD_ALLERGY", + "code": "MOLLUSCS", + "description": "Molluscs", + "listSequence": 7, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_MUSTARD", + "domain": "FOOD_ALLERGY", + "code": "MUSTARD", + "description": "Mustard", + "listSequence": 8, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_PEANUTS", + "domain": "FOOD_ALLERGY", + "code": "PEANUTS", + "description": "Peanuts", + "listSequence": 9, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_SESAME", + "domain": "FOOD_ALLERGY", + "code": "SESAME", + "description": "Sesame", + "listSequence": 10, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_SOYA", + "domain": "FOOD_ALLERGY", + "code": "SOYA", + "description": "Soya", + "listSequence": 11, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_SULPHUR_DIOXIDE", + "domain": "FOOD_ALLERGY", + "code": "SULPHUR_DIOXIDE", + "description": "Sulphur dioxide", + "listSequence": 12, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_TREE_NUTS", + "domain": "FOOD_ALLERGY", + "code": "TREE_NUTS", + "description": "Tree nuts", + "listSequence": 13, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_OTHER", + "domain": "FOOD_ALLERGY", + "code": "OTHER", + "description": "Other", + "listSequence": 14, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + } + ] + """.trimIndent(), + ) + } + } + } + + @DisplayName("GET /reference-data/domains/{domain}/codes/{code}") + @Nested + inner class GetReferenceDataCodeTest { + + @Nested + inner class Security { + + @Test + fun `access forbidden when no authority`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY/codes/MILK") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY/codes/MILK") + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))) + .exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can retrieve reference data code`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY/codes/MILK") + .headers(setAuthorisation(roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO"))) + .exchange() + .expectStatus().isOk + .expectBody().json( + """ + { + "id": "FOOD_ALLERGY_MILK", + "domain": "FOOD_ALLERGY", + "code": "MILK", + "description": "Milk", + "listSequence": 6, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + } + """.trimIndent(), + ) + } + } + + @Nested + inner class NotFound { + + @Test + fun `receive a 404 when no reference data code found`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY/codes/UNKNOWN") + .headers(setAuthorisation(roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO"))) + .exchange() + .expectStatus().isNotFound + } + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/ReferenceDataDomainControllerIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/ReferenceDataDomainControllerIntTest.kt new file mode 100644 index 0000000..9e44a84 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/controller/ReferenceDataDomainControllerIntTest.kt @@ -0,0 +1,297 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.controller + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.healthandmedication.integration.IntegrationTestBase + +class ReferenceDataDomainControllerIntTest : IntegrationTestBase() { + + @DisplayName("GET /reference-data/domains") + @Nested + inner class GetReferenceDataDomainsTest { + + @Nested + inner class Security { + + @Test + fun `access forbidden when no authority`() { + webTestClient.get().uri("/reference-data/domains") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.get().uri("/reference-data/domains") + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))) + .exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can retrieve reference data domains`() { + webTestClient.get().uri("/reference-data/domains") + .headers(setAuthorisation(roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO"))) + .exchange() + .expectStatus().isOk + .expectBody() + .jsonPath("$.length()").isEqualTo(2) + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].description").isEqualTo("Food allergy") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].listSequence").isEqualTo(0) + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].isActive").isEqualTo(true) + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].createdAt").isEqualTo("2025-01-16T00:00:00+0000") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].createdBy").isEqualTo("CONNECT_DPS") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes.length()").isEqualTo(15) + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[0].id").isEqualTo("FOOD_ALLERGY_CELERY") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[1].id").isEqualTo("FOOD_ALLERGY_GLUTEN") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[2].id").isEqualTo("FOOD_ALLERGY_CRUSTACEANS") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[3].id").isEqualTo("FOOD_ALLERGY_EGG") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[4].id").isEqualTo("FOOD_ALLERGY_FISH") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[5].id").isEqualTo("FOOD_ALLERGY_LUPIN") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[6].id").isEqualTo("FOOD_ALLERGY_MILK") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[7].id").isEqualTo("FOOD_ALLERGY_MOLLUSCS") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[8].id").isEqualTo("FOOD_ALLERGY_MUSTARD") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[9].id").isEqualTo("FOOD_ALLERGY_PEANUTS") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[10].id").isEqualTo("FOOD_ALLERGY_SESAME") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[11].id").isEqualTo("FOOD_ALLERGY_SOYA") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[12].id").isEqualTo("FOOD_ALLERGY_SULPHUR_DIOXIDE") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[13].id").isEqualTo("FOOD_ALLERGY_TREE_NUTS") + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].referenceDataCodes[14].id").isEqualTo("FOOD_ALLERGY_OTHER") + .jsonPath("$[?(@.code == 'MEDICAL_DIET')].description").isEqualTo("Medical diet") + .jsonPath("$[?(@.code == 'MEDICAL_DIET')].listSequence").isEqualTo(0) + .jsonPath("$[?(@.code == 'MEDICAL_DIET')].isActive").isEqualTo(true) + .jsonPath("$[?(@.code == 'MEDICAL_DIET')].createdAt").isEqualTo("2025-01-16T00:00:00+0000") + .jsonPath("$[?(@.code == 'MEDICAL_DIET')].createdBy").isEqualTo("CONNECT_DPS") + .jsonPath("$[?(@.code == 'MEDICAL_DIET')].referenceDataCodes.length()").isEqualTo(10) + } + } + + @Test + fun `can retrieve reference data domains including sub-domains at the top level`() { + webTestClient.get().uri("/reference-data/domains?includeSubDomains=true") + .headers(setAuthorisation(roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO"))) + .exchange() + .expectStatus().isOk + .expectBody() + .jsonPath("$.length()").isEqualTo(2) + .jsonPath("$[?(@.code == 'FOOD_ALLERGY')].isActive").isEqualTo(true) + } + } + + @DisplayName("GET /reference-data/domains/{domain}") + @Nested + inner class GetReferenceDataDomainTest { + + @Nested + inner class Security { + + @Test + fun `access forbidden when no authority`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY") + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))) + .exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can retrieve reference data domain`() { + webTestClient.get().uri("/reference-data/domains/FOOD_ALLERGY") + .headers(setAuthorisation(roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO"))) + .exchange() + .expectStatus().isOk + .expectBody().json( + """ + { + "code": "FOOD_ALLERGY", + "description": "Food allergy", + "listSequence": 0, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS", + "referenceDataCodes": [ + { + "id": "FOOD_ALLERGY_CELERY", + "domain": "FOOD_ALLERGY", + "code": "CELERY", + "description": "Celery", + "listSequence": 0, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_GLUTEN", + "domain": "FOOD_ALLERGY", + "code": "GLUTEN", + "description": "Cereals containing gluten", + "listSequence": 1, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_CRUSTACEANS", + "domain": "FOOD_ALLERGY", + "code": "CRUSTACEANS", + "description": "Crustaceans", + "listSequence": 2, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_EGG", + "domain": "FOOD_ALLERGY", + "code": "EGG", + "description": "Egg", + "listSequence": 3, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_FISH", + "domain": "FOOD_ALLERGY", + "code": "FISH", + "description": "Fish", + "listSequence": 4, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_LUPIN", + "domain": "FOOD_ALLERGY", + "code": "LUPIN", + "description": "Lupin", + "listSequence": 5, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_MILK", + "domain": "FOOD_ALLERGY", + "code": "MILK", + "description": "Milk", + "listSequence": 6, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_MOLLUSCS", + "domain": "FOOD_ALLERGY", + "code": "MOLLUSCS", + "description": "Molluscs", + "listSequence": 7, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_MUSTARD", + "domain": "FOOD_ALLERGY", + "code": "MUSTARD", + "description": "Mustard", + "listSequence": 8, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_PEANUTS", + "domain": "FOOD_ALLERGY", + "code": "PEANUTS", + "description": "Peanuts", + "listSequence": 9, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_SESAME", + "domain": "FOOD_ALLERGY", + "code": "SESAME", + "description": "Sesame", + "listSequence": 10, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_SOYA", + "domain": "FOOD_ALLERGY", + "code": "SOYA", + "description": "Soya", + "listSequence": 11, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_SULPHUR_DIOXIDE", + "domain": "FOOD_ALLERGY", + "code": "SULPHUR_DIOXIDE", + "description": "Sulphur dioxide", + "listSequence": 12, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_TREE_NUTS", + "domain": "FOOD_ALLERGY", + "code": "TREE_NUTS", + "description": "Tree nuts", + "listSequence": 13, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + }, + { + "id": "FOOD_ALLERGY_OTHER", + "domain": "FOOD_ALLERGY", + "code": "OTHER", + "description": "Other", + "listSequence": 14, + "isActive": true, + "createdAt": "2025-01-16T00:00:00+0000", + "createdBy": "CONNECT_DPS" + } + ] + } + """.trimIndent(), + ) + } + } + + @Nested + inner class NotFound { + + @Test + fun `receive a 404 when no reference data domain found`() { + webTestClient.get().uri("/reference-data/domains/UNKNOWN") + .headers(setAuthorisation(roles = listOf("ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO"))) + .exchange() + .expectStatus().isNotFound + } + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/IntegrationTestBase.kt index 3d1b8a2..0d9cc33 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/IntegrationTestBase.kt @@ -1,16 +1,24 @@ package uk.gov.justice.digital.hmpps.healthandmedication.integration +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT import org.springframework.http.HttpHeaders import org.springframework.test.web.reactive.server.WebTestClient +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.HmppsAuthApiExtension import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.PRISONER_NUMBER +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.PrisonerSearchExtension +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FieldMetadata +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.utils.HistoryComparison +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.utils.expectFieldHistory +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.utils.expectNoFieldHistoryFor import uk.gov.justice.hmpps.test.kotlin.auth.JwtAuthorisationHelper -@ExtendWith(HmppsAuthApiExtension::class) +@ExtendWith(HmppsAuthApiExtension::class, PrisonerSearchExtension::class) @SpringBootTest(webEnvironment = RANDOM_PORT) abstract class IntegrationTestBase : TestBase() { @@ -26,6 +34,21 @@ abstract class IntegrationTestBase : TestBase() { scopes: List = listOf("read"), ): (HttpHeaders) -> Unit = jwtAuthHelper.setAuthorisationHeader(username = username, scope = scopes, roles = roles) + protected fun expectFieldHistory(field: HealthAndMedicationField, vararg comparison: HistoryComparison) = + expectFieldHistory(field, fieldHistoryRepository.findAllByPrisonerNumber(PRISONER_NUMBER), *comparison) + + protected fun expectNoFieldHistoryFor(vararg field: HealthAndMedicationField) { + val history = fieldHistoryRepository.findAllByPrisonerNumber(PRISONER_NUMBER) + field.forEach { expectNoFieldHistoryFor(it, history) } + } + + protected fun expectFieldMetadata(prisonerNumber: String, vararg comparison: FieldMetadata) { + assertThat(fieldMetadataRepository.findAllByPrisonerNumber(prisonerNumber)).containsExactlyInAnyOrder(*comparison) + } + + protected fun expectFieldMetadata(vararg comparison: FieldMetadata) = + expectFieldMetadata(PRISONER_NUMBER, *comparison) + protected fun stubPingWithResponse(status: Int) { hmppsAuth.stubHealthPing(status) } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/TestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/TestBase.kt index bdee0c8..5cb5fd6 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/TestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/TestBase.kt @@ -1,16 +1,25 @@ package uk.gov.justice.digital.hmpps.healthandmedication.integration +import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.DynamicPropertyRegistry import org.springframework.test.context.DynamicPropertySource import uk.gov.justice.digital.hmpps.healthandmedication.config.FixedClock import uk.gov.justice.digital.hmpps.healthandmedication.integration.testcontainers.PostgresContainer +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.FieldHistoryRepository +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.FieldMetadataRepository import java.time.Instant import java.time.ZoneId @ActiveProfiles("test") abstract class TestBase { + @Autowired + lateinit var fieldHistoryRepository: FieldHistoryRepository + + @Autowired + lateinit var fieldMetadataRepository: FieldMetadataRepository + companion object { val clock: FixedClock = FixedClock( Instant.parse("2024-06-14T09:10:11.123+01:00"), diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/HmppsAuthMockServer.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/HmppsAuthMockServer.kt index 33531e2..3a038f8 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/HmppsAuthMockServer.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/HmppsAuthMockServer.kt @@ -29,6 +29,7 @@ class HmppsAuthApiExtension : override fun beforeEach(context: ExtensionContext) { hmppsAuth.resetRequests() + hmppsAuth.stubGrantToken() } override fun afterAll(context: ExtensionContext) { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/PrisonerSearchMockServer.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/PrisonerSearchMockServer.kt new file mode 100644 index 0000000..032318e --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/PrisonerSearchMockServer.kt @@ -0,0 +1,78 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.stubbing.StubMapping +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.dto.PrisonerDto + +internal const val PRISONER_NUMBER = "A1234AA" +internal const val PRISON_ID = "MDI" +internal const val PRISONER_NUMBER_NOT_FOUND = "NOT_FOUND" +internal const val PRISONER_NUMBER_THROW_EXCEPTION = "THROW" + +class PrisonerSearchServer : WireMockServer(WIREMOCK_PORT) { + companion object { + private const val WIREMOCK_PORT = 8112 + } + + private val mapper: ObjectMapper = jacksonObjectMapper().registerModule(JavaTimeModule()) + + fun stubHealthPing(status: Int) { + stubFor( + get("/health/ping").willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(if (status == 200) """{"status":"UP"}""" else """{"status":"DOWN"}""") + .withStatus(status), + ), + ) + } + + fun stubGetPrisoner(prisonNumber: String = PRISONER_NUMBER): StubMapping = + stubFor( + get("/prisoner/$prisonNumber") + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody( + mapper.writeValueAsString(PrisonerDto(prisonerNumber = prisonNumber, prisonId = PRISON_ID)), + ) + .withStatus(200), + ), + ) + + fun stubGetPrisonerException(prisonNumber: String = PRISONER_NUMBER_THROW_EXCEPTION): StubMapping = + stubFor(get("/prisoner/$prisonNumber").willReturn(aResponse().withStatus(500))) +} + +class PrisonerSearchExtension : + BeforeAllCallback, + AfterAllCallback, + BeforeEachCallback { + companion object { + @JvmField + val prisonerSearch = PrisonerSearchServer() + } + + override fun beforeAll(context: ExtensionContext) { + prisonerSearch.start() + } + + override fun beforeEach(context: ExtensionContext) { + prisonerSearch.resetRequests() + prisonerSearch.stubGetPrisoner() + prisonerSearch.stubGetPrisonerException() + } + + override fun afterAll(context: ExtensionContext) { + prisonerSearch.stop() + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldHistoryRepositoryTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldHistoryRepositoryTest.kt new file mode 100644 index 0000000..e7ef42a --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldHistoryRepositoryTest.kt @@ -0,0 +1,143 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.orm.jpa.JpaObjectRetrievalFailureException +import org.springframework.test.context.transaction.TestTransaction +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField.FOOD_ALLERGY +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FieldHistory +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import java.time.ZonedDateTime + +class FieldHistoryRepositoryTest : RepositoryTest() { + + @Test + fun `can persist field history - valueRef`() { + val fieldHistory = FieldHistory( + prisonerNumber = PRISONER_NUMBER, + field = FOOD_ALLERGY, + valueRef = REF_DATA_CODE, + createdAt = NOW, + createdBy = USER1, + ) + + val id = fieldHistoryRepository.save(fieldHistory).fieldHistoryId + + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + + with(fieldHistoryRepository.getReferenceById(id)) { + assertThat(prisonerNumber).isEqualTo(PRISONER_NUMBER) + assertThat(field).isEqualTo(FOOD_ALLERGY) + assertThat(valueInt).isNull() + assertThat(valueString).isNull() + assertThat(valueRef).isEqualTo(REF_DATA_CODE) + assertThat(createdAt).isEqualTo(NOW) + assertThat(createdBy).isEqualTo(USER1) + } + } + + @Test + fun `fails to persist field history if multiple valueXXX properties are set`() { + val fieldHistory = FieldHistory( + prisonerNumber = PRISONER_NUMBER, + field = FOOD_ALLERGY, + valueInt = 123, + valueRef = REF_DATA_CODE, + createdAt = NOW, + createdBy = USER1, + ) + + assertThrows(DataIntegrityViolationException::class.java) { + fieldHistoryRepository.save(fieldHistory).fieldHistoryId + + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + } + } + + @Test + fun `fails to persist field history if value_ref is not a valid value`() { + val fieldHistory = FieldHistory( + prisonerNumber = PRISONER_NUMBER, + field = FOOD_ALLERGY, + valueRef = INVALID_REF_DATA_CODE, + createdAt = NOW, + createdBy = USER1, + ) + + assertThrows(JpaObjectRetrievalFailureException::class.java) { + fieldHistoryRepository.save(fieldHistory).fieldHistoryId + + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + } + } + + @Test + fun `can check for equality`() { + assertThat( + FieldHistory( + prisonerNumber = PRISONER_NUMBER, + field = FOOD_ALLERGY, + valueRef = REF_DATA_CODE, + createdAt = NOW, + createdBy = USER1, + ), + ).isEqualTo( + FieldHistory( + prisonerNumber = PRISONER_NUMBER, + field = FOOD_ALLERGY, + valueRef = REF_DATA_CODE, + createdAt = NOW, + createdBy = USER1, + ), + ) + } + + @Test + fun `toString does not cause a stack overflow`() { + assertThat( + FieldHistory( + prisonerNumber = PRISONER_NUMBER, + field = FOOD_ALLERGY, + valueRef = REF_DATA_CODE, + createdAt = NOW, + createdBy = USER1, + ).toString(), + ).isInstanceOf(String::class.java) + } + + private companion object { + const val PRISONER_NUMBER = "A1234AA" + const val USER1 = "USER1" + + val REF_DATA_DOMAIN = ReferenceDataDomain("FOOD_ALLERGY", "Food allergy", 0, ZonedDateTime.now(), "OMS_OWNER") + val REF_DATA_CODE = ReferenceDataCode( + id = "FOOD_ALLERGY_PEANUTS", + domain = REF_DATA_DOMAIN, + code = "PEANUTS", + description = "Peanuts", + listSequence = 9, + createdAt = ZonedDateTime.now(), + createdBy = "OMS_OWNER", + ) + val INVALID_REF_DATA_CODE = ReferenceDataCode( + id = "FOOD_ALLERGY_INVALID", + domain = REF_DATA_DOMAIN, + code = "INVALID", + description = "INVALID", + listSequence = 1, + createdAt = ZonedDateTime.now(), + createdBy = "testUser", + ) + + val NOW: ZonedDateTime = ZonedDateTime.now(clock) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldMetadataTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldMetadataTest.kt new file mode 100644 index 0000000..5f2a577 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/FieldMetadataTest.kt @@ -0,0 +1,96 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.test.context.transaction.TestTransaction +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField.FOOD_ALLERGY +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FieldMetadata +import java.time.ZonedDateTime + +class FieldMetadataTest : RepositoryTest() { + + @Test + fun `can persist field metadata`() { + fieldMetadataRepository.save( + FieldMetadata( + PRISONER_NUMBER, + FOOD_ALLERGY, + lastModifiedAt = NOW, + lastModifiedBy = USER1, + ), + ) + + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + + with(fieldMetadataRepository.findAllByPrisonerNumber(PRISONER_NUMBER)[0]) { + assertThat(prisonerNumber).isEqualTo(PRISONER_NUMBER) + assertThat(field).isEqualTo(FOOD_ALLERGY) + assertThat(lastModifiedAt).isEqualTo(NOW) + assertThat(lastModifiedBy).isEqualTo(USER1) + } + } + + @Test + fun `can update field metadata`() { + fieldMetadataRepository.save( + FieldMetadata( + PRISONER_NUMBER, + FOOD_ALLERGY, + lastModifiedAt = NOW, + lastModifiedBy = USER1, + ), + ) + + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + + val fieldMetadata = fieldMetadataRepository.findAllByPrisonerNumber(PRISONER_NUMBER)[0] + fieldMetadata.lastModifiedAt = NOW.plusDays(1) + fieldMetadata.lastModifiedBy = USER2 + + fieldMetadataRepository.save(fieldMetadata) + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + + with(fieldMetadataRepository.findAllByPrisonerNumber(PRISONER_NUMBER)[0]) { + assertThat(prisonerNumber).isEqualTo(PRISONER_NUMBER) + assertThat(field).isEqualTo(FOOD_ALLERGY) + assertThat(lastModifiedAt).isEqualTo(NOW.plusDays(1)) + assertThat(lastModifiedBy).isEqualTo(USER2) + } + } + + @Test + fun `can check for equality`() { + assertThat( + FieldMetadata(PRISONER_NUMBER, FOOD_ALLERGY, lastModifiedAt = NOW, lastModifiedBy = USER1), + ).isEqualTo( + FieldMetadata(PRISONER_NUMBER, FOOD_ALLERGY, lastModifiedAt = NOW, lastModifiedBy = USER1), + ) + + assertThat( + FieldMetadata(PRISONER_NUMBER, FOOD_ALLERGY, lastModifiedAt = NOW, lastModifiedBy = USER1), + ).isNotEqualTo( + FieldMetadata("Z1234ZZ", FOOD_ALLERGY, lastModifiedAt = NOW, lastModifiedBy = USER1), + ) + } + + @Test + fun `toString does not cause stack overflow`() { + assertThat( + FieldMetadata(PRISONER_NUMBER, FOOD_ALLERGY, lastModifiedAt = NOW, lastModifiedBy = USER1).toString(), + ).isInstanceOf(String::class.java) + } + + private companion object { + const val PRISONER_NUMBER = "A1234AA" + const val USER1 = "USER1" + const val USER2 = "USER2" + + val NOW: ZonedDateTime = ZonedDateTime.now(clock) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/PrisonerHealthRepositoryTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/PrisonerHealthRepositoryTest.kt new file mode 100644 index 0000000..c6d6b4b --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/PrisonerHealthRepositoryTest.kt @@ -0,0 +1,189 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.transaction.TestTransaction +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FoodAllergy +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.MedicalDietaryRequirement +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.PrisonerHealth +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import java.time.ZonedDateTime + +class PrisonerHealthRepositoryTest : RepositoryTest() { + + @Autowired + lateinit var repository: PrisonerHealthRepository + + fun save(prisonerHealth: PrisonerHealth) { + repository.save(prisonerHealth) + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + } + + @Test + fun `can persist health`() { + val prisonerHealth = PrisonerHealth( + PRISONER_NUMBER, + mutableSetOf(EGG_ALLERGY, MILK_ALLERGY), + mutableSetOf(MEDICAL_DIET_LACTOSE_INTOLERANT, MEDICAL_DIET_NUTRIENT_DEFICIENCY), + ) + save(prisonerHealth) + + with(repository.getReferenceById(PRISONER_NUMBER)) { + assertThat(prisonerNumber).isEqualTo(PRISONER_NUMBER) + assertThat(foodAllergies).hasSize(2) + assertThat(foodAllergies).contains(EGG_ALLERGY) + assertThat(foodAllergies).contains(MILK_ALLERGY) + assertThat(medicalDietaryRequirements).hasSize(2) + assertThat(medicalDietaryRequirements).contains(MEDICAL_DIET_LACTOSE_INTOLERANT) + assertThat(medicalDietaryRequirements).contains(MEDICAL_DIET_NUTRIENT_DEFICIENCY) + } + } + + @Test + fun `can persist health with null values`() { + val prisonerHealth = PrisonerHealth(PRISONER_NUMBER) + save(prisonerHealth) + + with(repository.getReferenceById(PRISONER_NUMBER)) { + assertThat(prisonerNumber).isEqualTo(PRISONER_NUMBER) + assertThat(foodAllergies).isEmpty() + assertThat(medicalDietaryRequirements).isEmpty() + } + } + + @Test + fun `can update health`() { + repository.save(PrisonerHealth(PRISONER_NUMBER)) + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + + val health = repository.getReferenceById(PRISONER_NUMBER) + health.foodAllergies.add(EGG_ALLERGY) + health.medicalDietaryRequirements.add(MEDICAL_DIET_LACTOSE_INTOLERANT) + + repository.save(health) + TestTransaction.flagForCommit() + TestTransaction.end() + TestTransaction.start() + + with(repository.getReferenceById(PRISONER_NUMBER)) { + assertThat(prisonerNumber).isEqualTo(PRISONER_NUMBER) + assertThat(foodAllergies).hasSize(1) + assertThat(foodAllergies.first()).isEqualTo(EGG_ALLERGY) + assertThat(medicalDietaryRequirements).hasSize(1) + assertThat(medicalDietaryRequirements.first()).isEqualTo(MEDICAL_DIET_LACTOSE_INTOLERANT) + } + } + + @Test + fun `can test for equality`() { + assertThat( + PrisonerHealth( + prisonerNumber = PRISONER_NUMBER, + foodAllergies = mutableSetOf(EGG_ALLERGY), + medicalDietaryRequirements = mutableSetOf(MEDICAL_DIET_NUTRIENT_DEFICIENCY), + ), + ).isEqualTo( + PrisonerHealth( + prisonerNumber = PRISONER_NUMBER, + foodAllergies = mutableSetOf(EGG_ALLERGY), + medicalDietaryRequirements = mutableSetOf(MEDICAL_DIET_NUTRIENT_DEFICIENCY), + ), + ) + + // Prisoner number + assertThat(PrisonerHealth(PRISONER_NUMBER)).isNotEqualTo(PrisonerHealth("Example")) + + // Allergies + assertThat(PrisonerHealth(PRISONER_NUMBER, mutableSetOf(EGG_ALLERGY))).isNotEqualTo( + PrisonerHealth(PRISONER_NUMBER, mutableSetOf(MILK_ALLERGY)), + ) + + // Medical diet + assertThat( + PrisonerHealth(PRISONER_NUMBER, mutableSetOf(EGG_ALLERGY), mutableSetOf(MEDICAL_DIET_LACTOSE_INTOLERANT)), + ).isNotEqualTo( + PrisonerHealth(PRISONER_NUMBER, mutableSetOf(EGG_ALLERGY), mutableSetOf(MEDICAL_DIET_NUTRIENT_DEFICIENCY)), + ) + } + + @Test + fun `toString does not cause stack overflow`() { + assertThat( + PrisonerHealth(PRISONER_NUMBER, mutableSetOf(EGG_ALLERGY), mutableSetOf(MEDICAL_DIET_LACTOSE_INTOLERANT)).toString(), + ).isInstanceOf(String::class.java) + } + + private companion object { + const val PRISONER_NUMBER = "A1234AA" + + val FOOD_ALLERGY_DOMAIN = ReferenceDataDomain("FOOD_ALLERGY", "Food allergy", 0, ZonedDateTime.now(), "OMS_OWNER") + val EGG_ALLERGY = FoodAllergy( + prisonerNumber = PRISONER_NUMBER, + allergy = ReferenceDataCode( + id = "FOOD_ALLERGY_EGG", + domain = FOOD_ALLERGY_DOMAIN, + code = "EGG", + description = "Egg", + listSequence = 3, + createdAt = ZonedDateTime.now(), + createdBy = "OMS_OWNER", + ), + ) + + val MILK_ALLERGY = + FoodAllergy( + prisonerNumber = PRISONER_NUMBER, + allergy = ReferenceDataCode( + id = "FOOD_ALLERGY_MILK", + domain = FOOD_ALLERGY_DOMAIN, + code = "MILK", + description = "Milk", + listSequence = 6, + createdAt = ZonedDateTime.now(), + createdBy = "OMS_OWNER", + ), + ) + + val MEDICAL_DIET_DOMAIN = ReferenceDataDomain( + "MEDICAL_DIET", + "Medical diet", + 0, + ZonedDateTime.now(), + "CONNECT_DPS", + ) + + val MEDICAL_DIET_NUTRIENT_DEFICIENCY = + MedicalDietaryRequirement( + prisonerNumber = PRISONER_NUMBER, + dietaryRequirement = ReferenceDataCode( + id = "MEDICAL_DIET_NUTRIENT_DEFICIENCY", + domain = MEDICAL_DIET_DOMAIN, + code = "NUTRIENT_DEFICIENCY", + description = "Nutrient deficiency", + listSequence = 7, + createdAt = ZonedDateTime.now(), + createdBy = "CONNECT_DPS", + ), + ) + + val MEDICAL_DIET_LACTOSE_INTOLERANT = + MedicalDietaryRequirement( + prisonerNumber = PRISONER_NUMBER, + dietaryRequirement = ReferenceDataCode( + id = "MEDICAL_DIET_LACTOSE_INTOLERANT", + domain = MEDICAL_DIET_DOMAIN, + code = "LACTOSE_INTOLERANT", + description = "Lactose intolerant", + listSequence = 5, + createdAt = ZonedDateTime.now(), + createdBy = "CONNECT_DPS", + ), + ) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/RepositoryTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/RepositoryTest.kt new file mode 100644 index 0000000..9d53840 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/RepositoryTest.kt @@ -0,0 +1,19 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE +import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.test.context.jdbc.Sql +import org.springframework.test.context.jdbc.SqlMergeMode +import org.springframework.test.context.jdbc.SqlMergeMode.MergeMode.MERGE +import org.springframework.transaction.annotation.Transactional +import uk.gov.justice.digital.hmpps.healthandmedication.integration.TestBase + +@DataJpaTest +@AutoConfigureJson +@Transactional +@AutoConfigureTestDatabase(replace = NONE) +@SqlMergeMode(MERGE) +@Sql("classpath:jpa/repository/reset.sql") +abstract class RepositoryTest : TestBase() diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/utils/FieldHistoryTestUtils.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/utils/FieldHistoryTestUtils.kt new file mode 100644 index 0000000..d78b80b --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/utils/FieldHistoryTestUtils.kt @@ -0,0 +1,42 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.utils + +import org.assertj.core.api.Assertions.assertThat +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FieldHistory +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.HistoryItem +import java.time.ZonedDateTime + +data class HistoryComparison( + val value: T?, + val createdAt: ZonedDateTime, + val createdBy: String, +) + +fun expectNoFieldHistoryFor(field: HealthAndMedicationField, history: Collection) { + val fieldHistory = history.filter { it.field == field }.toList() + assertThat(fieldHistory).isEmpty() +} + +fun assertHistoryItemEqual( + expected: HistoryComparison, + actual: HistoryItem, +) { + assertThat(actual.createdAt).isEqualTo(expected.createdAt) + assertThat(actual.createdBy).isEqualTo(expected.createdBy) +} + +// Refactor this for history items so that history item tests can all use it regardless of the fields +fun expectFieldHistory( + field: HealthAndMedicationField, + history: Collection, + vararg comparison: HistoryComparison, +) { + val fieldHistory = history.filter { it.field == field }.toList() + assertThat(fieldHistory).hasSize(comparison.size) + fieldHistory.forEachIndexed { index, actual -> + val expected = comparison[index] + assertThat(actual.field).isEqualTo(field) + assertThat(field.get(actual)).isEqualTo(expected.value) + assertHistoryItemEqual(expected, actual) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataCodeMapperTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataCodeMapperTest.kt new file mode 100644 index 0000000..e008bbc --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataCodeMapperTest.kt @@ -0,0 +1,69 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.mapper + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import java.time.ZonedDateTime + +class ReferenceDataCodeMapperTest { + + private val testDomain = ReferenceDataDomain("TEST", "Test domain", 1, ZonedDateTime.now(), "testUser") + private val testCode = ReferenceDataCode( + id = "${testDomain}_ORANGE", + domain = testDomain, + code = "ORANGE", + description = "Orange", + listSequence = 1, + createdAt = ZonedDateTime.now(), + createdBy = "testUser", + ) + + @Test + fun `test toDto with default description`() { + val referenceDataCode = ReferenceDataCode( + id = "${testDomain}_ORANGE", + domain = testDomain, + code = "ORANGE", + description = "Orange", + listSequence = 1, + createdAt = ZonedDateTime.now(), + createdBy = "testUser", + ) + + val dto = referenceDataCode.toDto() + + assertThat(dto.domain).isEqualTo("TEST") + assertThat(dto.code).isEqualTo("ORANGE") + assertThat(dto.description).isEqualTo("Orange") // Should use default description + } + + @Test + fun `test isActive when deactivatedAt is null`() { + val referenceDataCode = testCode + + assertThat(referenceDataCode.isActive()).isEqualTo(true) + } + + @Test + fun `test isActive when deactivatedAt is in the future`() { + val referenceDataCode = testCode + referenceDataCode.lastModifiedAt = ZonedDateTime.now() + referenceDataCode.lastModifiedBy = "testUser" + referenceDataCode.deactivatedAt = ZonedDateTime.now().plusDays(1) + referenceDataCode.deactivatedBy = "testUser" + + assertThat(referenceDataCode.isActive()).isEqualTo(true) + } + + @Test + fun `test isActive when deactivatedAt is in the past`() { + val referenceDataCode = testCode + referenceDataCode.lastModifiedAt = ZonedDateTime.now() + referenceDataCode.lastModifiedBy = "testUser" + referenceDataCode.deactivatedAt = ZonedDateTime.now().minusDays(1) + referenceDataCode.deactivatedBy = "testUser" + + assertThat(referenceDataCode.isActive()).isEqualTo(false) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataDomainMapperTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataDomainMapperTest.kt new file mode 100644 index 0000000..cda5ec3 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataDomainMapperTest.kt @@ -0,0 +1,64 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.mapper + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import java.time.ZonedDateTime + +class ReferenceDataDomainMapperTest { + + private val testDomain = ReferenceDataDomain("TEST_DOMAIN", "Domain description", 1, ZonedDateTime.now(), "testUser") + private val testCode = ReferenceDataCode( + id = "${testDomain}_TEST_CODE", + domain = testDomain, + code = "TEST_CODE", + description = "Code description", + listSequence = 1, + createdAt = ZonedDateTime.now(), + createdBy = "testUser", + ) + + @Test + fun `toDto`() { + val referenceDataDomain = testDomain + testDomain.referenceDataCodes = mutableListOf(testCode) + + val dto = referenceDataDomain.toDto() + + assertThat(dto.code).isEqualTo("TEST_DOMAIN") + assertThat(dto.description).isEqualTo("Domain description") + assertThat(dto.referenceDataCodes.size).isEqualTo(1) + assertThat(dto.referenceDataCodes.first().code).isEqualTo("TEST_CODE") + assertThat(dto.referenceDataCodes.first().description).isEqualTo("Code description") + } + + @Test + fun `isActive when deactivatedAt is null`() { + val referenceDataDomain = testCode + + assertThat(referenceDataDomain.isActive()).isEqualTo(true) + } + + @Test + fun `isActive when deactivatedAt is in the future`() { + val referenceDataDomain = testCode + referenceDataDomain.lastModifiedAt = ZonedDateTime.now() + referenceDataDomain.lastModifiedBy = "testUser" + referenceDataDomain.deactivatedAt = ZonedDateTime.now().plusDays(1) + referenceDataDomain.deactivatedBy = "testUser" + + assertThat(referenceDataDomain.isActive()).isEqualTo(true) + } + + @Test + fun `isActive when deactivatedAt is in the past`() { + val referenceDataDomain = testCode + referenceDataDomain.lastModifiedAt = ZonedDateTime.now() + referenceDataDomain.lastModifiedBy = "testUser" + referenceDataDomain.deactivatedAt = ZonedDateTime.now().minusDays(1) + referenceDataDomain.deactivatedBy = "testUser" + + assertThat(referenceDataDomain.isActive()).isEqualTo(false) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/PrisonerHealthServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/PrisonerHealthServiceTest.kt new file mode 100644 index 0000000..13d032d --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/PrisonerHealthServiceTest.kt @@ -0,0 +1,357 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.service + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness.LENIENT +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.PrisonerSearchClient +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.dto.PrisonerDto +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataSimpleDto +import uk.gov.justice.digital.hmpps.healthandmedication.dto.request.PrisonerHealthUpdateRequest +import uk.gov.justice.digital.hmpps.healthandmedication.dto.response.HealthDto +import uk.gov.justice.digital.hmpps.healthandmedication.dto.response.ValueWithMetadata +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField.FOOD_ALLERGY +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField.MEDICAL_DIET +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FieldMetadata +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FoodAllergies +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FoodAllergy +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.JsonObject +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.MedicalDietaryRequirement +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.MedicalDietaryRequirements +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.PrisonerHealth +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.PrisonerHealthRepository +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.utils.HistoryComparison +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.utils.expectFieldHistory +import uk.gov.justice.digital.hmpps.healthandmedication.mapper.toSimpleDto +import uk.gov.justice.digital.hmpps.healthandmedication.utils.AuthenticationFacade +import java.time.Clock +import java.time.ZonedDateTime +import java.util.Optional + +@ExtendWith(MockitoExtension::class) +@MockitoSettings(strictness = LENIENT) +class PrisonerHealthServiceTest { + @Mock + lateinit var prisonerHealthRepository: PrisonerHealthRepository + + @Mock + lateinit var prisonerSearchClient: PrisonerSearchClient + + @Mock + lateinit var referenceDataCodeRepository: ReferenceDataCodeRepository + + @Mock + lateinit var authenticationFacade: AuthenticationFacade + + @Spy + val clock: Clock? = Clock.fixed(NOW.toInstant(), NOW.zone) + + @InjectMocks + lateinit var underTest: PrisonerHealthService + + private val savedPrisonerHealth = argumentCaptor() + + @BeforeEach + fun beforeEach() { + whenever(referenceDataCodeRepository.findById(FOOD_REFERENCE_DATA_CODE_ID)).thenReturn(Optional.of(EGG_ALLERGY)) + whenever(referenceDataCodeRepository.findById(LOW_FAT_REFERENCE_DATA_CODE_ID)).thenReturn( + Optional.of( + LOW_FAT_REFERENCE_DATA_CODE, + ), + ) + whenever(authenticationFacade.getUserOrSystemInContext()).thenReturn(USER1) + } + + @Test + fun `health data not found`() { + whenever(prisonerHealthRepository.findById(PRISONER_NUMBER)).thenReturn(Optional.empty()) + + val result = underTest.getHealth(PRISONER_NUMBER) + assertThat(result).isNull() + } + + @Test + fun `prison health data is found`() { + whenever(prisonerHealthRepository.findById(PRISONER_NUMBER)).thenReturn( + Optional.of( + PrisonerHealth( + prisonerNumber = PRISONER_NUMBER, + medicalDietaryRequirements = mutableSetOf( + LOW_FAT_DIET_REQUIREMENT, + ), + foodAllergies = mutableSetOf( + EGG_FOOD_ALLERGY, + ), + fieldMetadata = mutableMapOf( + MEDICAL_DIET to FieldMetadata( + PRISONER_NUMBER, + MEDICAL_DIET, + NOW, + USER1, + ), + FOOD_ALLERGY to FieldMetadata( + PRISONER_NUMBER, + MEDICAL_DIET, + NOW, + USER1, + ), + ), + ), + ), + ) + + val result = underTest.getHealth(PRISONER_NUMBER) + + assertThat(result).isEqualTo( + HealthDto( + medicalDietaryRequirements = ValueWithMetadata( + listOf( + ReferenceDataSimpleDto( + id = LOW_FAT_REFERENCE_DATA_CODE_ID, + description = LOW_FAT_DIET_REQUIREMENT.dietaryRequirement.description, + listSequence = LOW_FAT_DIET_REQUIREMENT.dietaryRequirement.listSequence, + isActive = true, + ), + ), + NOW, + USER1, + ), + foodAllergies = ValueWithMetadata( + listOf( + ReferenceDataSimpleDto( + id = EGG_ALLERGY.id, + description = EGG_ALLERGY.description, + listSequence = EGG_ALLERGY.listSequence, + isActive = true, + ), + ), + NOW, + USER1, + ), + ), + ) + } + + @Nested + inner class CreateOrUpdatePrisonerHealth { + + @BeforeEach + fun beforeEach() { + whenever(prisonerHealthRepository.save(savedPrisonerHealth.capture())).thenAnswer { savedPrisonerHealth.firstValue } + } + + @Test + fun `creating new health data`() { + whenever(prisonerSearchClient.getPrisoner(PRISONER_NUMBER)).thenReturn( + PRISONER_SEARCH_RESPONSE, + ) + + whenever(prisonerHealthRepository.findById(PRISONER_NUMBER)).thenReturn(Optional.empty()) + + assertThat( + underTest.createOrUpdate( + PRISONER_NUMBER, + HEALTH_UPDATE_REQUEST, + ), + ).isEqualTo( + HealthDto( + foodAllergies = ValueWithMetadata(listOf(EGG_ALLERGY.toSimpleDto()), NOW, USER1), + medicalDietaryRequirements = ValueWithMetadata(listOf(LOW_FAT_REFERENCE_DATA_CODE.toSimpleDto()), NOW, USER1), + ), + ) + + with(savedPrisonerHealth.firstValue) { + assertThat(prisonerNumber).isEqualTo(PRISONER_NUMBER) + assertThat(foodAllergies).containsAll(listOf(EGG_FOOD_ALLERGY)) + assertThat(medicalDietaryRequirements).containsAll(listOf(LOW_FAT_DIET_REQUIREMENT)) + + expectFieldHistory( + MEDICAL_DIET, + fieldHistory, + HistoryComparison( + value = JsonObject( + field = MEDICAL_DIET, + value = MedicalDietaryRequirements(medicalDietaryRequirements = listOf(LOW_FAT_REFERENCE_DATA_CODE_ID)), + ), + createdAt = NOW, + createdBy = USER1, + ), + ) + expectFieldHistory( + FOOD_ALLERGY, + fieldHistory, + HistoryComparison( + value = JsonObject( + field = FOOD_ALLERGY, + value = FoodAllergies(listOf(EGG_ALLERGY.id)), + ), + createdAt = NOW, + createdBy = USER1, + ), + ) + } + } + + @Test + fun `updating health data`() { + whenever(prisonerSearchClient.getPrisoner(PRISONER_NUMBER)).thenReturn( + PRISONER_SEARCH_RESPONSE, + ) + + whenever(prisonerHealthRepository.findById(PRISONER_NUMBER)).thenReturn( + Optional.of( + PrisonerHealth( + prisonerNumber = PRISONER_NUMBER, + foodAllergies = mutableSetOf(EGG_FOOD_ALLERGY), + medicalDietaryRequirements = mutableSetOf(LOW_FAT_DIET_REQUIREMENT), + ).also { it.updateFieldHistory(lastModifiedAt = NOW.minusDays(1), lastModifiedBy = USER2) }, + ), + ) + + assertThat(underTest.createOrUpdate(PRISONER_NUMBER, HEALTH_UPDATE_REQUEST_WITH_NULL)).isEqualTo( + HealthDto( + foodAllergies = ValueWithMetadata(emptyList(), NOW, USER1), + medicalDietaryRequirements = ValueWithMetadata(emptyList(), NOW, USER1), + ), + ) + + fun firstHistory(value: T): HistoryComparison = HistoryComparison( + value = value, + createdAt = NOW.minusDays(1), + createdBy = USER2, + ) + + fun secondHistory(value: T): HistoryComparison = HistoryComparison( + value = value, + createdAt = NOW, + createdBy = USER1, + ) + + with(savedPrisonerHealth.firstValue) { + assertThat(prisonerNumber).isEqualTo(PRISONER_NUMBER) + assertThat(foodAllergies).isEqualTo(mutableSetOf()) + assertThat(medicalDietaryRequirements).isEqualTo(mutableSetOf()) + + expectFieldHistory( + MEDICAL_DIET, + fieldHistory, + firstHistory( + JsonObject( + field = MEDICAL_DIET, + value = MedicalDietaryRequirements(medicalDietaryRequirements = listOf(LOW_FAT_REFERENCE_DATA_CODE_ID)), + ), + ), + secondHistory( + JsonObject( + field = MEDICAL_DIET, + value = MedicalDietaryRequirements(medicalDietaryRequirements = emptyList()), + ), + ), + ) + + expectFieldHistory( + FOOD_ALLERGY, + fieldHistory, + firstHistory( + JsonObject( + field = FOOD_ALLERGY, + value = FoodAllergies(listOf(EGG_ALLERGY.id)), + ), + ), + secondHistory( + JsonObject( + field = FOOD_ALLERGY, + value = FoodAllergies(emptyList()), + ), + ), + ) + } + } + } + + private companion object { + const val PRISONER_NUMBER = "A1234AA" + const val USER1 = "USER1" + const val USER2 = "USER2" + + val NOW = ZonedDateTime.now() + + val REFERENCE_DATA_CODE_ID = "EXAMPLE_CODE" + val REFERENCE_DATA_CODE_DESCRPTION = "Example code" + val REFERENCE_DATA_LIST_SEQUENCE = 0 + + val FOOD_REFERENCE_DATA_CODE_ID = "FOOD_EXAMPLE_CODE" + val FOOD_REFERENCE_DATA_CODE = "FOOD_CODE" + val FOOD_REFERENCE_DATA_CODE_DESCRPTION = "Example food code" + val FOOD_REFERENCE_DATA_LIST_SEQUENCE = 0 + val FOOD_REFERENCE_DATA_DOMAIN_CODE = "FOOD_EXAMPLE" + val FOOD_REFERENCE_DATA_DOMAIN_DESCRIPTION = "Food Example" + + val EGG_ALLERGY = ReferenceDataCode( + id = FOOD_REFERENCE_DATA_CODE_ID, + code = FOOD_REFERENCE_DATA_CODE, + createdBy = USER1, + createdAt = NOW, + description = FOOD_REFERENCE_DATA_CODE_DESCRPTION, + listSequence = FOOD_REFERENCE_DATA_LIST_SEQUENCE, + domain = ReferenceDataDomain( + code = FOOD_REFERENCE_DATA_DOMAIN_CODE, + createdBy = USER1, + createdAt = NOW, + listSequence = FOOD_REFERENCE_DATA_LIST_SEQUENCE, + description = FOOD_REFERENCE_DATA_DOMAIN_DESCRIPTION, + ), + ) + + val EGG_FOOD_ALLERGY = FoodAllergy(prisonerNumber = PRISONER_NUMBER, allergy = EGG_ALLERGY) + + val LOW_FAT_REFERENCE_DATA_CODE_ID = "MEDICAL_DIET_LOW_FAT" + val LOW_FAT_REFERENCE_DATA_CODE = ReferenceDataCode( + id = LOW_FAT_REFERENCE_DATA_CODE_ID, + code = "LOW_FAT", + createdBy = USER1, + createdAt = NOW, + description = "Example medical diet code", + listSequence = 0, + domain = ReferenceDataDomain( + code = "MEDICAL_DIET", + createdBy = USER1, + createdAt = NOW, + listSequence = 0, + description = "Example medical diet domain", + ), + ) + + val LOW_FAT_DIET_REQUIREMENT = MedicalDietaryRequirement( + prisonerNumber = PRISONER_NUMBER, + dietaryRequirement = LOW_FAT_REFERENCE_DATA_CODE, + ) + + val PRISONER_SEARCH_RESPONSE = PrisonerDto(PRISONER_NUMBER) + + val attributes = mutableMapOf( + Pair("foodAllergies", listOf(FOOD_REFERENCE_DATA_CODE_ID)), + Pair("medicalDietaryRequirements", listOf(LOW_FAT_REFERENCE_DATA_CODE_ID)), + ) + + val HEALTH_UPDATE_REQUEST = PrisonerHealthUpdateRequest(attributes) + + val attributes_undefined = mutableMapOf( + Pair("foodAllergies", emptyList()), + Pair("medicalDietaryRequirements", emptyList()), + ) + val HEALTH_UPDATE_REQUEST_WITH_NULL = PrisonerHealthUpdateRequest(attributes_undefined) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataCodeServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataCodeServiceTest.kt new file mode 100644 index 0000000..788f72c --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataCodeServiceTest.kt @@ -0,0 +1,107 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.service + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.whenever +import uk.gov.justice.digital.hmpps.healthandmedication.config.ReferenceDataCodeNotFoundException +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import java.time.ZonedDateTime + +@ExtendWith(MockitoExtension::class) +class ReferenceDataCodeServiceTest { + + @Mock + private lateinit var referenceDataCodeRepository: ReferenceDataCodeRepository + + @InjectMocks + private lateinit var referenceDataCodeService: ReferenceDataCodeService + + @Test + fun `test getReferenceDataCodes with includeInactive`() { + val activeCode = ACTIVE_CODE + val inactiveCode = INACTIVE_CODE + whenever(referenceDataCodeRepository.findAllByDomainAndIncludeInactive(DOMAIN.code, true)).thenReturn( + listOf( + activeCode, + inactiveCode, + ), + ) + + val result = referenceDataCodeService.getReferenceDataCodes(DOMAIN.code, true) + + assertThat(result.size).isEqualTo(2) + verify(referenceDataCodeRepository).findAllByDomainAndIncludeInactive(DOMAIN.code, true) + } + + @Test + fun `test getReferenceDataCodes without includeInactive`() { + val activeCode = ACTIVE_CODE + whenever(referenceDataCodeRepository.findAllByDomainAndIncludeInactive(DOMAIN.code, false)).thenReturn( + listOf( + activeCode, + ), + ) + + val result = referenceDataCodeService.getReferenceDataCodes(DOMAIN.code, false) + + assertThat(result.size).isEqualTo(1) + verify(referenceDataCodeRepository).findAllByDomainAndIncludeInactive(DOMAIN.code, false) + } + + @Test + fun `test getReferenceDataCode found`() { + val code = "ACTIVE" + val referenceDataCode = ACTIVE_CODE + + whenever(referenceDataCodeRepository.findByCodeAndDomainCode(code, DOMAIN.code)).thenReturn( + referenceDataCode, + ) + + val result = referenceDataCodeService.getReferenceDataCode(code, DOMAIN.code) + + assertNotNull(result) + assertThat(result.code).isEqualTo(code) + verify(referenceDataCodeRepository).findByCodeAndDomainCode(code, DOMAIN.code) + } + + @Test + fun `test getReferenceDataCode not found`() { + val code = "NONEXISTENT" + whenever(referenceDataCodeRepository.findByCodeAndDomainCode(code, DOMAIN.code)).thenReturn(null) + + val exception = assertThrows(ReferenceDataCodeNotFoundException::class.java) { + referenceDataCodeService.getReferenceDataCode(code, DOMAIN.code) + } + + assertThat(exception.message).isEqualTo("No data for code 'NONEXISTENT' in domain 'DOMAIN'") + verify(referenceDataCodeRepository).findByCodeAndDomainCode(code, DOMAIN.code) + } + + private companion object { + val DOMAIN = ReferenceDataDomain("DOMAIN", "Domain", 0, ZonedDateTime.now(), "testUser") + val ACTIVE_CODE = + ReferenceDataCode("${DOMAIN.code}_ACTIVE", "ACTIVE", DOMAIN, "Active domain", 0, ZonedDateTime.now(), "testUser") + val INACTIVE_CODE = + ReferenceDataCode( + "${DOMAIN.code}_INACTIVE", + "INACTIVE", + DOMAIN, + "Inactive domain", + 0, + ZonedDateTime.now(), + "testUser", + ).apply { + this.deactivatedAt = ZonedDateTime.now() + this.deactivatedBy = "testUser" + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataDomainServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataDomainServiceTest.kt new file mode 100644 index 0000000..29b0049 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataDomainServiceTest.kt @@ -0,0 +1,104 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.service + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.whenever +import uk.gov.justice.digital.hmpps.healthandmedication.config.ReferenceDataDomainNotFoundException +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataDomainRepository +import java.time.ZonedDateTime + +@ExtendWith(MockitoExtension::class) +class ReferenceDataDomainServiceTest { + + @Mock + private lateinit var referenceDataDomainRepository: ReferenceDataDomainRepository + + @InjectMocks + private lateinit var referenceDataDomainService: ReferenceDataDomainService + + @Test + fun `test getReferenceDataDomains with includeInactive`() { + val activeDomain = ACTIVE_DOMAIN + val inactiveDomain = INACTIVE_DOMAIN + whenever(referenceDataDomainRepository.findAllByIncludeInactive(true)).thenReturn( + listOf( + activeDomain, + inactiveDomain, + ), + ) + + val result = referenceDataDomainService.getReferenceDataDomains(true) + + assertThat(result.size).isEqualTo(2) + verify(referenceDataDomainRepository).findAllByIncludeInactive(true) + } + + @Test + fun `test getReferenceDataDomains without includeInactive`() { + val activeDomain = ACTIVE_DOMAIN + whenever(referenceDataDomainRepository.findAllByIncludeInactive(false)).thenReturn(listOf(activeDomain)) + + val result = referenceDataDomainService.getReferenceDataDomains(false) + + assertThat(result.size).isEqualTo(1) + verify(referenceDataDomainRepository).findAllByIncludeInactive(false) + } + + @Test + fun `test getReferenceDataDomains with includeSubDomains`() { + val activeDomain = ACTIVE_DOMAIN + whenever(referenceDataDomainRepository.findAllByIncludeInactive(false, true)).thenReturn( + listOf( + activeDomain, + ), + ) + + val result = referenceDataDomainService.getReferenceDataDomains(false, true) + + assertThat(result.size).isEqualTo(1) + verify(referenceDataDomainRepository).findAllByIncludeInactive(false, true) + } + + @Test + fun `test getReferenceDataDomain found`() { + val code = "ACTIVE" + val referenceDataDomain = ACTIVE_DOMAIN + + whenever(referenceDataDomainRepository.findByCode(code)).thenReturn(referenceDataDomain) + + val result = referenceDataDomainService.getReferenceDataDomain(code) + + assertNotNull(result) + assertThat(result.code).isEqualTo(code) + verify(referenceDataDomainRepository).findByCode(code) + } + + @Test + fun `test getReferenceDataDomain not found`() { + val code = "NONEXISTENT" + whenever(referenceDataDomainRepository.findByCode(code)).thenReturn(null) + + val exception = assertThrows(ReferenceDataDomainNotFoundException::class.java) { + referenceDataDomainService.getReferenceDataDomain(code) + } + + assertThat(exception.message).isEqualTo("No data for domain 'NONEXISTENT'") + verify(referenceDataDomainRepository).findByCode(code) + } + + private companion object { + val ACTIVE_DOMAIN = ReferenceDataDomain("ACTIVE", "Active domain", 0, ZonedDateTime.now(), "testUser") + val INACTIVE_DOMAIN = ReferenceDataDomain("INACTIVE", "Inactive domain", 0, ZonedDateTime.now(), "testUser").apply { + this.deactivatedAt = ZonedDateTime.now() + this.deactivatedBy = "testUser" + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/NullishTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/NullishTest.kt new file mode 100644 index 0000000..cd0462e --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/NullishTest.kt @@ -0,0 +1,54 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.utils + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class NullishTest { + @Test + fun `should return Defined when attribute is present and of correct type`() { + val attributes = mapOf("key1" to "value1") + val result = getAttributeAsNullish(attributes, "key1") + assertThat(Nullish.Defined("value1")).isEqualTo(result) + } + + @Test + fun `should return Defined with null when attribute is present and value is null`() { + val attributes = mapOf("key1" to null) + val result = getAttributeAsNullish(attributes, "key1") + assertThat(Nullish.Defined(null)).isEqualTo(result) + } + + @Test + fun `should return Undefined when attribute is not present in the map`() { + val attributes = mapOf("key1" to "value1") + val result = getAttributeAsNullish(attributes, "key2") + assertThat(Nullish.Undefined).isEqualTo(result) + } + + @Test + fun `should throw IllegalArgumentException when attribute is present but of wrong type`() { + val attributes = mapOf("key1" to 123) + assertThatThrownBy { getAttributeAsNullish(attributes, "key1") } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("key1 is not an instance of String") + } + + @Test + fun `should not run function provided when attribute is not present in the map`() { + val attributes = mapOf("key1" to 123) + var check = "unchanged" + getAttributeAsNullish(attributes, "key2").let { check = "changed" } + + assertThat(check).isEqualTo("unchanged") + } + + @Test + fun `should run function provided when attribute is present in the map`() { + val attributes = mapOf("key1" to "changed") + var check: String? = "unchanged" + getAttributeAsNullish(attributes, "key1").let { check = it } + + assertThat(check).isEqualTo("changed") + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/PrisonerNumberUtilsTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/PrisonerNumberUtilsTest.kt new file mode 100644 index 0000000..666923d --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/PrisonerNumberUtilsTest.kt @@ -0,0 +1,43 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.utils + +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.whenever +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.PrisonerSearchClient +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.dto.PrisonerDto + +@ExtendWith(MockitoExtension::class) +class PrisonerNumberUtilsTest { + @Mock + lateinit var prisonerSearchClient: PrisonerSearchClient + + @Test + fun `should not throw exception when prisoner number exists`() { + val prisonerNumber = PRISONER_NUMBER + + whenever(prisonerSearchClient.getPrisoner(prisonerNumber)) + .thenReturn(PRISONER_DTO) + + validatePrisonerNumber(prisonerSearchClient, prisonerNumber) + } + + @Test + fun `should throw IllegalArgumentException when prisoner number does not exist`() { + val prisonerNumber = PRISONER_NUMBER + + whenever(prisonerSearchClient.getPrisoner(prisonerNumber)) + .thenReturn(null) + + assertThatThrownBy { validatePrisonerNumber(prisonerSearchClient, prisonerNumber) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("Prisoner number '$prisonerNumber' not found") + } + + private companion object { + const val PRISONER_NUMBER = "A1234BC" + val PRISONER_DTO = PrisonerDto(PRISONER_NUMBER) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/ReferenceCodeUtilsTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/ReferenceCodeUtilsTest.kt new file mode 100644 index 0000000..1b97d9a --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/ReferenceCodeUtilsTest.kt @@ -0,0 +1,58 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.utils + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.whenever +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import java.time.ZonedDateTime +import java.util.Optional + +@ExtendWith(MockitoExtension::class) +class ReferenceCodeUtilsTest { + + @Mock + lateinit var referenceDataCodeRepository: ReferenceDataCodeRepository + + @Test + fun `should return null when id is null`() { + val result = toReferenceDataCode(referenceDataCodeRepository, null) + assertThat(result).isNull() + } + + @Test + fun `should return ReferenceDataCode when id is not null and found in repository`() { + val id = ID + val expectedReferenceDataCode = REFERENCE_DATA_CODE + + whenever(referenceDataCodeRepository.findById(id)) + .thenReturn(Optional.of(expectedReferenceDataCode)) + + val result = toReferenceDataCode(referenceDataCodeRepository, id) + assertThat(expectedReferenceDataCode).isEqualTo(result) + } + + @Test + fun `should throw IllegalArgumentException when id is not null and not found in repository`() { + val id = "123" + + whenever(referenceDataCodeRepository.findById(id)) + .thenReturn(Optional.empty()) + + assertThatThrownBy { toReferenceDataCode(referenceDataCodeRepository, id) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("Invalid reference data code: $id") + } + + private companion object { + const val ID = "DOMAIN_CODE" + val DOMAIN = ReferenceDataDomain("DOMAIN", "Domain", 0, ZonedDateTime.now(), "testUser") + val REFERENCE_DATA_CODE = + ReferenceDataCode(ID, "CODE", DOMAIN, "Ref data", 0, ZonedDateTime.now(), "testUser") + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishRangeValidatorTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishRangeValidatorTest.kt new file mode 100644 index 0000000..bdab845 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishRangeValidatorTest.kt @@ -0,0 +1,36 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.validator + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishRange +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Defined +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Undefined + +class NullishRangeValidatorTest { + + private lateinit var validator: NullishRangeValidator + + @BeforeEach + fun setUp() { + validator = NullishRangeValidator() + validator.initialize(NullishRange(min = 5, max = 10)) + } + + @Test + fun `valid values`() { + assertThat(validator.isValid(Undefined, null)).isTrue() + assertThat(validator.isValid(Defined(null), null)).isTrue() + assertThat(validator.isValid(Defined(5), null)).isTrue() + assertThat(validator.isValid(Defined(10), null)).isTrue() + assertThat(validator.isValid(Defined(7), null)).isTrue() + } + + @Test + fun `invalid values`() { + assertThat(validator.isValid(Defined(4), null)).isFalse() + assertThat(validator.isValid(Defined(11), null)).isFalse() + assertThat(validator.isValid(Defined(-1), null)).isFalse() + assertThat(validator.isValid(Defined(0), null)).isFalse() + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeListValidatorTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeListValidatorTest.kt new file mode 100644 index 0000000..14c5797 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeListValidatorTest.kt @@ -0,0 +1,71 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.validator + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness.LENIENT +import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishReferenceDataCodeList +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Defined +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Undefined + +@ExtendWith(MockitoExtension::class) +@MockitoSettings(strictness = LENIENT) +class NullishReferenceDataCodeListValidatorTest { + + @Mock + lateinit var referenceDataCodeRepository: ReferenceDataCodeRepository + + @InjectMocks + lateinit var validator: NullishReferenceDataCodeListValidator + + @BeforeEach + fun setUp() { + whenever(referenceDataCodeRepository.findAllByDomainAndIncludeInactive("EXAMPLE_DOMAIN", false)).thenReturn( + ACTIVE_CODES, + ) + + validator.initialize(NullishReferenceDataCodeList(arrayOf("EXAMPLE_DOMAIN"))) + } + + @Test + fun `valid values`() { + assertThat(validator.isValid(Undefined, null)).isTrue() + assertThat(validator.isValid(Defined(listOf("EXAMPLE_DOMAIN_CODE")), null)).isTrue() + assertThat(validator.isValid(Defined(listOf("EXAMPLE_DOMAIN_TWO")), null)).isTrue() + assertThat(validator.isValid(Defined(listOf("EXAMPLE_DOMAIN_CODE", "EXAMPLE_DOMAIN_TWO")), null)).isTrue() + } + + @Test + fun `invalid values`() { + assertThat(validator.isValid(Defined(null), null)).isFalse() + assertThat(validator.isValid(Defined(listOf("EXAMPLE_DOMAIN_FAKE")), null)).isFalse() + assertThat(validator.isValid(Defined(listOf("EXAMPLE_DOMAIN_CODE", "EXAMPLE_DOMAIN_FAKE")), null)).isFalse() + } + + private companion object { + val ACTIVE_CODES = listOf("EXAMPLE_DOMAIN_CODE", "EXAMPLE_DOMAIN_TWO").map { + ReferenceDataCode( + id = it, + domain = ReferenceDataDomain( + code = "EXAMPLE_DOMAIN", + description = "", + listSequence = 0, + createdBy = "", + ), + code = "CODE", + description = "", + listSequence = 0, + createdBy = "", + ) + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeValidatorTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeValidatorTest.kt new file mode 100644 index 0000000..b23c6d9 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeValidatorTest.kt @@ -0,0 +1,99 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.validator + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness.LENIENT +import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Defined +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Undefined +import java.util.Optional + +@ExtendWith(MockitoExtension::class) +@MockitoSettings(strictness = LENIENT) +class NullishReferenceDataCodeValidatorTest { + + @Mock + lateinit var referenceDataCodeRepository: ReferenceDataCodeRepository + + @InjectMocks + lateinit var validator: NullishReferenceDataCodeValidator + + @BeforeEach + fun setUp() { + whenever(referenceDataCodeRepository.findById("EXAMPLE_DOMAIN_CODE")).thenReturn( + Optional.of( + ReferenceDataCode( + id = "EXAMPLE_DOMAIN_CODE", + domain = ReferenceDataDomain( + code = "EXAMPLE_DOMAIN", + description = "", + listSequence = 0, + createdBy = "", + ), + code = "CODE", + description = "", + listSequence = 0, + createdBy = "", + ), + ), + ) + + whenever(referenceDataCodeRepository.findById("EXAMPLE_TWO_CODE")).thenReturn( + Optional.of( + ReferenceDataCode( + id = "EXAMPLE_TWO_CODE", + domain = ReferenceDataDomain( + code = "OTHER_DOMAIN", + description = "", + listSequence = 0, + createdBy = "", + ), + code = "CODE", + description = "", + listSequence = 0, + createdBy = "", + ), + ), + ) + } + + @Test + fun `valid values (allowNull=true)`() { + validator.initialize(NullishReferenceDataCode(domains = arrayOf("EXAMPLE_DOMAIN"), allowNull = true)) + assertThat(validator.isValid(Undefined, null)).isTrue() + assertThat(validator.isValid(Defined("EXAMPLE_DOMAIN_CODE"), null)).isTrue() + assertThat(validator.isValid(Defined(null), null)).isTrue() + } + + @Test + fun `invalid values (allowNull=true)`() { + validator.initialize(NullishReferenceDataCode(domains = arrayOf("EXAMPLE_DOMAIN"), allowNull = true)) + assertThat(validator.isValid(Defined("NON_EXISTING_CODE"), null)).isFalse() + assertThat(validator.isValid(Defined("EXAMPLE_TWO_CODE"), null)).isFalse() + } + + @Test + fun `valid values (allowNull=false)`() { + validator.initialize(NullishReferenceDataCode(domains = arrayOf("EXAMPLE_DOMAIN"), allowNull = false)) + assertThat(validator.isValid(Undefined, null)).isTrue() + assertThat(validator.isValid(Defined("EXAMPLE_DOMAIN_CODE"), null)).isTrue() + } + + @Test + fun `invalid values (allowNull=false)`() { + validator.initialize(NullishReferenceDataCode(domains = arrayOf("EXAMPLE_DOMAIN"), allowNull = true)) + assertThat(validator.isValid(Defined("NON_EXISTING_CODE"), null)).isFalse() + assertThat(validator.isValid(Defined("EXAMPLE_TWO_CODE"), null)).isFalse() + assertThat(validator.isValid(Defined(null), null)).isTrue() + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 288a3bc..2cf0e8a 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -9,9 +9,6 @@ api: hmpps-auth.base-url: http://localhost:8090/auth prisoner-search.base-url: http://localhost:8112 -hmpps-auth: - url: "http://localhost:8090/auth" - spring: datasource: url: jdbc:postgresql://localhost:5432/health-and-medication-data diff --git a/src/test/resources/jpa/repository/reset.sql b/src/test/resources/jpa/repository/reset.sql new file mode 100644 index 0000000..7e34f63 --- /dev/null +++ b/src/test/resources/jpa/repository/reset.sql @@ -0,0 +1,5 @@ +DELETE FROM field_history; +DELETE FROM field_metadata; +DELETE FROM health; +DELETE FROM food_allergy; +DELETE FROM medical_dietary_requirement; diff --git a/src/test/resources/resource/healthandmedication/field_history.sql b/src/test/resources/resource/healthandmedication/field_history.sql new file mode 100644 index 0000000..0b864d6 --- /dev/null +++ b/src/test/resources/resource/healthandmedication/field_history.sql @@ -0,0 +1,4 @@ +INSERT INTO field_history (field_history_id, prisoner_number, field, value_ref, value_json, created_at, created_by) +VALUES +(-201, 'A1234AA', 'MEDICAL_DIET', null, '{"field": "MEDICAL_DIET", "value": { "medicalDietaryRequirements": ["MEDICAL_DIET_LOW_CHOLESTEROL"] }}', '2024-01-02 09:10:11.123', 'USER1'), +(-202, 'A1234AA', 'FOOD_ALLERGY', null, '{"field": "FOOD_ALLERGY", "value": { "allergies": ["FOOD_ALLERGY_SOYA"] }}', '2024-01-02 09:10:11.123', 'USER1'); diff --git a/src/test/resources/resource/healthandmedication/field_metadata.sql b/src/test/resources/resource/healthandmedication/field_metadata.sql new file mode 100644 index 0000000..35992d3 --- /dev/null +++ b/src/test/resources/resource/healthandmedication/field_metadata.sql @@ -0,0 +1,5 @@ +INSERT INTO field_metadata(prisoner_number, field, last_modified_at, last_modified_by) +VALUES +('A1234AA', 'MEDICAL_DIET', '2024-01-02 09:10:11.123', 'USER1'), +('A1234AA', 'FOOD_ALLERGY', '2024-01-02 09:10:11.123', 'USER1'); + diff --git a/src/test/resources/resource/healthandmedication/food_allergies.sql b/src/test/resources/resource/healthandmedication/food_allergies.sql new file mode 100644 index 0000000..cc8c2df --- /dev/null +++ b/src/test/resources/resource/healthandmedication/food_allergies.sql @@ -0,0 +1,2 @@ +INSERT INTO food_allergy (prisoner_number, allergy) +VALUES ('A1234AA', 'FOOD_ALLERGY_SOYA'); diff --git a/src/test/resources/resource/healthandmedication/health.sql b/src/test/resources/resource/healthandmedication/health.sql new file mode 100644 index 0000000..93750cb --- /dev/null +++ b/src/test/resources/resource/healthandmedication/health.sql @@ -0,0 +1,2 @@ +INSERT INTO health (prisoner_number, smoker_or_vaper) +VALUES ('A1234AA', null); diff --git a/src/test/resources/resource/healthandmedication/medical_dietary_requirements.sql b/src/test/resources/resource/healthandmedication/medical_dietary_requirements.sql new file mode 100644 index 0000000..40d24a2 --- /dev/null +++ b/src/test/resources/resource/healthandmedication/medical_dietary_requirements.sql @@ -0,0 +1,2 @@ +INSERT INTO medical_dietary_requirement (prisoner_number, dietary_requirement) +VALUES ('A1234AA', 'MEDICAL_DIET_LOW_CHOLESTEROL');