diff --git a/build.gradle.kts b/build.gradle.kts index 783b5fb..39367d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,9 @@ plugins { id("uk.gov.justice.hmpps.gradle-spring-boot") version "6.1.0" kotlin("plugin.spring") version "2.0.21" + kotlin("plugin.jpa") version "2.0.21" + jacoco + idea } configurations { @@ -8,11 +11,24 @@ configurations { } dependencies { + + // Spring Boot implementation("uk.gov.justice.service.hmpps:hmpps-kotlin-spring-boot-starter:1.1.0") implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + + // OpenAPI implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0") + // Database + runtimeOnly("com.zaxxer:HikariCP") + runtimeOnly("org.flywaydb:flyway-database-postgresql") + runtimeOnly("org.postgresql:postgresql") + + // Test testImplementation("uk.gov.justice.service.hmpps:hmpps-kotlin-spring-boot-starter-test:1.1.0") + testImplementation("org.testcontainers:junit-jupiter:1.20.3") + testImplementation("org.testcontainers:postgresql:1.20.3") testImplementation("org.wiremock:wiremock-standalone:3.9.2") testImplementation("io.swagger.parser.v3:swagger-parser:2.1.24") { exclude(group = "io.swagger.core.v3") @@ -28,3 +44,15 @@ tasks { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21 } } + +// Jacoco code coverage +tasks.named("test") { + finalizedBy("jacocoTestReport") +} + +tasks.named("jacocoTestReport") { + reports { + html.required.set(true) + xml.required.set(true) + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 6430c5b..c67a763 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,8 +13,6 @@ services: environment: - SERVER_PORT=8080 - HMPPS_AUTH_URL=http://hmpps-auth:8080/auth - # TODO: Remove this URL and replace with outgoing service URLs - - EXAMPLE_URL=http://hmpps-health-and-medication-api:8080 - SPRING_PROFILES_ACTIVE=dev hmpps-auth: @@ -31,5 +29,18 @@ services: - SPRING_PROFILES_ACTIVE=dev - APPLICATION_AUTHENTICATION_UI_ALLOWLIST=0.0.0.0/0 + health-and-medication-data-db: + image: postgres + networks: + - hmpps + container_name: health-and-medication-data-db + restart: unless-stopped + ports: + - "9432:5432" + environment: + - POSTGRES_PASSWORD=health-and-medication-data + - POSTGRES_USER=health-and-medication-data + - POSTGRES_DB=health-and-medication-data + - TZ="Europe/London" networks: hmpps: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d888240..fe96863 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,10 +27,17 @@ spring: hibernate: ddl-auto: none + flyway: + enabled: true + url: ${spring.datasource.url} + user: ${database.username} + password: ${database.password} + locations: classpath:/db/migration/common + datasource: - url: 'jdbc:postgresql://${DATABASE_ENDPOINT}/${DATABASE_NAME}?sslmode=verify-full' - username: ${DATABASE_USERNAME} - password: ${DATABASE_PASSWORD} + url: 'jdbc:postgresql://${database.endpoint}/${database.name}?sslmode=verify-full' + username: ${database.username} + password: ${database.password} hikari: pool-name: HEALTH-AND-MEDICATION-DB-CP maximum-pool-size: 10 diff --git a/src/main/resources/db/migration/common/placeholder b/src/main/resources/db/migration/common/placeholder new file mode 100644 index 0000000..e69de29 diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/FixedClock.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/FixedClock.kt new file mode 100644 index 0000000..e779203 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/FixedClock.kt @@ -0,0 +1,17 @@ +package uk.gov.justice.digital.hmpps.healthandmedicationapi.config + +import java.time.Clock +import java.time.Instant +import java.time.ZoneId +import java.time.temporal.TemporalAmount + +class FixedClock(var instant: Instant, private var zone: ZoneId) : Clock() { + + fun elapse(amountToAdd: TemporalAmount) { + instant += amountToAdd + } + + override fun instant(): Instant = this.instant + override fun withZone(zone: ZoneId): Clock = this.apply { this.zone = zone } + override fun getZone(): ZoneId = this.zone +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/IntegrationTestBase.kt index b231409..8bfb36a 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/IntegrationTestBase.kt @@ -5,7 +5,6 @@ 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.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.wiremock.HmppsAuthApiExtension import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth @@ -13,8 +12,7 @@ import uk.gov.justice.hmpps.test.kotlin.auth.JwtAuthorisationHelper @ExtendWith(HmppsAuthApiExtension::class) @SpringBootTest(webEnvironment = RANDOM_PORT) -@ActiveProfiles("test") -abstract class IntegrationTestBase { +abstract class IntegrationTestBase : TestBase() { @Autowired protected lateinit var webTestClient: WebTestClient diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/TestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/TestBase.kt new file mode 100644 index 0000000..9a70a9d --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/TestBase.kt @@ -0,0 +1,33 @@ +package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration + +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertySource +import uk.gov.justice.digital.hmpps.healthandmedicationapi.config.FixedClock +import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.testcontainers.PostgresContainer +import java.time.Instant +import java.time.ZoneId + +@ActiveProfiles("test") +abstract class TestBase { + + companion object { + val clock: FixedClock = FixedClock( + Instant.parse("2024-06-14T09:10:11.123+01:00"), + ZoneId.of("Europe/London"), + ) + + private val pgContainer = PostgresContainer.instance + + @JvmStatic + @DynamicPropertySource + fun properties(registry: DynamicPropertyRegistry) { + pgContainer?.run { + registry.add("spring.datasource.url", pgContainer::getJdbcUrl) + registry.add("spring.flyway.url", pgContainer::getJdbcUrl) + registry.add("spring.flyway.user", pgContainer::getUsername) + registry.add("spring.flyway.password", pgContainer::getPassword) + } + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/testcontainers/PostgresContainer.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/testcontainers/PostgresContainer.kt new file mode 100644 index 0000000..2dc9b84 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/testcontainers/PostgresContainer.kt @@ -0,0 +1,40 @@ +package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.testcontainers + +import org.slf4j.LoggerFactory +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.containers.wait.strategy.Wait +import java.io.IOException +import java.net.ServerSocket + +object PostgresContainer { + val instance: PostgreSQLContainer? by lazy { startPostgresqlContainer() } + + private fun startPostgresqlContainer(): PostgreSQLContainer? { + if (isPostgresRunning()) { + log.warn("Using existing Postgres database") + return null + } + log.info("Creating a Postgres database") + return PostgreSQLContainer("postgres").apply { + withEnv("HOSTNAME_EXTERNAL", "localhost") + withEnv("PORT_EXTERNAL", "5432") + withDatabaseName("health-and-medication-data") + withUsername("health-and-medication-data") + withPassword("health-and-medication-data") + setWaitStrategy(Wait.forListeningPort()) + withReuse(true) + + start() + } + } + + private fun isPostgresRunning(): Boolean = + try { + val serverSocket = ServerSocket(5432) + serverSocket.localPort == 0 + } catch (e: IOException) { + true + } + + private val log = LoggerFactory.getLogger(this::class.java) +}