diff --git a/.nais/nais.yaml b/.nais/nais.yaml index 12c6b7b..fd4decf 100644 --- a/.nais/nais.yaml +++ b/.nais/nais.yaml @@ -39,3 +39,23 @@ spec: - application: dp-saksbehandling kafka: pool: {{ kafka_pool }} + gcp: + sqlInstances: + - collation: nb_NO.UTF8 + databases: + - envVarPrefix: DB + name: oppslag_journalpostid + diskAutoresize: true + diskType: SSD + highAvailability: { { db.highAvailability } } + insights: + enabled: true + queryStringLength: 4500 + recordApplicationTags: true + recordClientAddress: true + maintenance: + day: 1 + hour: 4 + pointInTimeRecovery: { { db.pointInTimeRecovery } } + tier: { { db.tier } } + type: POSTGRES_15 diff --git a/build.gradle.kts b/build.gradle.kts index e95996e..9fdc3c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { testImplementation(libs.mockk) testImplementation(libs.kotest.assertions.core) testImplementation("io.ktor:ktor-server-test-host-jvm:${libs.versions.ktor.get()}") + testImplementation(libs.bundles.postgres.test) } application { diff --git a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/ApplicationBuilder.kt b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/ApplicationBuilder.kt index d20d7fe..43de2ce 100644 --- a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/ApplicationBuilder.kt +++ b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/ApplicationBuilder.kt @@ -1,6 +1,7 @@ package no.nav.dagpenger.oppslag.journalpost.id import mu.KotlinLogging +import no.nav.dagpenger.oppslag.journalpost.id.PostgresDataSourceBuilder.runMigration import no.nav.helse.rapids_rivers.RapidApplication import no.nav.helse.rapids_rivers.RapidsConnection @@ -9,7 +10,7 @@ internal class ApplicationBuilder(config: Map) : RapidsConnectio private val logger = KotlinLogging.logger { } } - val repository = InmemoryRepository() + val repository = InMemoryJournalpostRepository() private val rapidsConnection: RapidsConnection = RapidApplication.Builder(RapidApplication.RapidApplicationConfig.fromEnv(config)) @@ -32,7 +33,7 @@ internal class ApplicationBuilder(config: Map) : RapidsConnectio } override fun onStartup(rapidsConnection: RapidsConnection) { - // runMigration() + runMigration() logger.info { "Starter opp dp-oppslag-journalpost-id" } } diff --git a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/InnsendingFerdigstiltMottak.kt b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/InnsendingFerdigstiltMottak.kt index ff35de1..f2f923a 100644 --- a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/InnsendingFerdigstiltMottak.kt +++ b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/InnsendingFerdigstiltMottak.kt @@ -10,7 +10,7 @@ import java.util.UUID private val logger = KotlinLogging.logger { } -class InnsendingFerdigstiltMottak(rapidsConnection: RapidsConnection, private val repository: Repository) : +class InnsendingFerdigstiltMottak(rapidsConnection: RapidsConnection, private val journalpostRepository: JournalpostRepository) : River.PacketListener { init { River(rapidsConnection).apply { @@ -28,7 +28,7 @@ class InnsendingFerdigstiltMottak(rapidsConnection: RapidsConnection, private va ) { val journalpostId = packet["journalpostId"].asText() val søknadId = packet["søknadsData.søknad_uuid"].asUUID() - repository.lagre(søknadId, journalpostId) + journalpostRepository.lagre(søknadId, journalpostId) logger.info { "Lagret $søknadId -> $journalpostId" } } } diff --git a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostIdApi.kt b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostIdApi.kt index 48fc205..33d61f9 100644 --- a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostIdApi.kt +++ b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostIdApi.kt @@ -10,13 +10,13 @@ import io.ktor.server.routing.route import io.ktor.server.routing.routing import java.util.UUID -fun Application.journalpostApi(repository: Repository) { +fun Application.journalpostApi(journalpostRepository: JournalpostRepository) { apiConfig() routing { route("v1/journalpost/{søknadId}") { get { - call.respond(HttpStatusCode.OK, repository.hent(call.søknadId())) + call.respond(HttpStatusCode.OK, journalpostRepository.hent(call.søknadId())) } } } diff --git a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/Repository.kt b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostRepository.kt similarity index 71% rename from src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/Repository.kt rename to src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostRepository.kt index 231d78d..d4c5390 100644 --- a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/Repository.kt +++ b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostRepository.kt @@ -2,7 +2,7 @@ package no.nav.dagpenger.oppslag.journalpost.id import java.util.UUID -interface Repository { +interface JournalpostRepository { fun lagre( søknadId: UUID, journalpostId: String, @@ -13,7 +13,7 @@ interface Repository { class JournalpostIkkeFunnet(msg: String) : RuntimeException(msg) } -class InmemoryRepository : Repository { +class InMemoryJournalpostRepository : JournalpostRepository { private val storage = mutableMapOf() override fun lagre( @@ -25,5 +25,5 @@ class InmemoryRepository : Repository { override fun hent(søknadId: UUID): String = storage[søknadId] - ?: throw Repository.JournalpostIkkeFunnet("Fant ikke journalpost for søknadId $søknadId") + ?: throw JournalpostRepository.JournalpostIkkeFunnet("Fant ikke journalpost for søknadId $søknadId") } diff --git a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresDataSourceBuilder.kt b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresDataSourceBuilder.kt new file mode 100644 index 0000000..38fe89f --- /dev/null +++ b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresDataSourceBuilder.kt @@ -0,0 +1,58 @@ +package no.nav.dagpenger.oppslag.journalpost.id + +import ch.qos.logback.core.util.OptionHelper.getEnv +import ch.qos.logback.core.util.OptionHelper.getSystemProperty +import com.zaxxer.hikari.HikariDataSource +import org.flywaydb.core.Flyway +import org.flywaydb.core.api.configuration.FluentConfiguration + +// Understands how to create a data source from environment variables +internal object PostgresDataSourceBuilder { + const val DB_USERNAME_KEY = "DB_USERNAME" + const val DB_PASSWORD_KEY = "DB_PASSWORD" + const val DB_DATABASE_KEY = "DB_DATABASE" + const val DB_HOST_KEY = "DB_HOST" + const val DB_PORT_KEY = "DB_PORT" + + private fun getOrThrow(key: String): String = getEnv(key) ?: getSystemProperty(key) + + val dataSource by lazy { + HikariDataSource().apply { + dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" + addDataSourceProperty("serverName", getOrThrow(DB_HOST_KEY)) + addDataSourceProperty("portNumber", getOrThrow(DB_PORT_KEY)) + addDataSourceProperty("databaseName", getOrThrow(DB_DATABASE_KEY)) + addDataSourceProperty("user", getOrThrow(DB_USERNAME_KEY)) + addDataSourceProperty("password", getOrThrow(DB_PASSWORD_KEY)) + maximumPoolSize = 10 + minimumIdle = 1 + idleTimeout = 10001 + connectionTimeout = 1000 + maxLifetime = 30001 + } + } + + private fun flyWayBuilder() = Flyway.configure().connectRetries(10) + + private val flyWayBuilder: FluentConfiguration = Flyway.configure().connectRetries(10) + + fun clean() = flyWayBuilder.cleanDisabled(false).dataSource(dataSource).load().clean() + + internal fun runMigration(initSql: String? = null): Int = + flyWayBuilder + .dataSource(dataSource) + .initSql(initSql) + .load() + .migrate() + .migrations + .size + + internal fun runMigrationTo(target: String): Int = + flyWayBuilder() + .dataSource(dataSource) + .target(target) + .load() + .migrate() + .migrations + .size +} diff --git a/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresJournalpostRepository.kt b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresJournalpostRepository.kt new file mode 100644 index 0000000..15570f3 --- /dev/null +++ b/src/main/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresJournalpostRepository.kt @@ -0,0 +1,53 @@ +package no.nav.dagpenger.oppslag.journalpost.id + +import kotliquery.queryOf +import kotliquery.sessionOf +import java.util.UUID +import javax.sql.DataSource + +class PostgresJournalpostRepository(private val dataSource: DataSource) : JournalpostRepository { + override fun lagre( + søknadId: UUID, + journalpostId: String, + ) { + sessionOf(dataSource).use { session -> + session.run( + queryOf( + //language=PostgreSQL + statement = + """ + INSERT INTO soknad_id_journalpost_id_mapping_v1 + (soknad_id, journalpost_id) + VALUES + (:soknad_id, :journalpost_id) + ON CONFLICT (soknad_id) DO NOTHING + """.trimIndent(), + paramMap = + mapOf( + "soknad_id" to søknadId, + "journalpost_id" to journalpostId, + ), + ).asUpdate, + ) + } + } + + override fun hent(søknadId: UUID): String { + sessionOf(dataSource).use { session -> + return session.run( + queryOf( + //language=PostgreSQL + statement = + """ + SELECT journalpost_id + FROM soknad_id_journalpost_id_mapping_v1 + WHERE soknad_id = :soknad_id + """.trimIndent(), + paramMap = mapOf("soknad_id" to søknadId), + ).map { row -> + row.string("journalpost_id") + }.asSingle, + ) ?: throw JournalpostRepository.JournalpostIkkeFunnet("Fant ikke journapostId for søknadId: $søknadId") + } + } +} diff --git a/src/main/resources/db/migration/V1__CREATE_TABLES.sql b/src/main/resources/db/migration/V1__CREATE_TABLES.sql new file mode 100644 index 0000000..b4177ff --- /dev/null +++ b/src/main/resources/db/migration/V1__CREATE_TABLES.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS soknad_id_journalpost_id_mapping_v1 +( + soknad_id UUID PRIMARY KEY, + journalpost_id TEXT NOT NULL, + registrert_tidspunkt TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/InnsendingFerdigstiltMottakTest.kt b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/InnsendingFerdigstiltMottakTest.kt index 6962e31..dbcd56f 100644 --- a/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/InnsendingFerdigstiltMottakTest.kt +++ b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/InnsendingFerdigstiltMottakTest.kt @@ -14,12 +14,12 @@ class InnsendingFerdigstiltMottakTest { @Test fun `Skal ta tak i riktige pakker`() { - val repository = - mockk().also { + val journalpostRepository = + mockk().also { every { it.lagre(any(), any()) } just runs } - InnsendingFerdigstiltMottak(testRapid, repository) + InnsendingFerdigstiltMottak(testRapid, journalpostRepository) //language=JSON testRapid.sendTestMessage( @@ -36,6 +36,6 @@ class InnsendingFerdigstiltMottakTest { """.trimIndent(), ) - verify(exactly = 1) { repository.lagre(UUID.fromString("f0509e9a-f913-45cb-9aa7-ed7bafcb9e93"), "662317896") } + verify(exactly = 1) { journalpostRepository.lagre(UUID.fromString("f0509e9a-f913-45cb-9aa7-ed7bafcb9e93"), "662317896") } } } diff --git a/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostidApiTest.kt b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostIdApiTest.kt similarity index 76% rename from src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostidApiTest.kt rename to src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostIdApiTest.kt index 07d6482..a757ac0 100644 --- a/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostidApiTest.kt +++ b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/JournalpostIdApiTest.kt @@ -10,16 +10,16 @@ import io.mockk.mockk import org.junit.jupiter.api.Test import java.util.UUID.randomUUID -class JournalpostidApiTest { +class JournalpostIdApiTest { @Test fun `Skal kunne finne journalpost id fra søknad id`() { val journalpostId = "123" val søknadId = randomUUID() val repository = - InmemoryRepository().also { + InMemoryJournalpostRepository().also { it.lagre(søknadId, journalpostId) } - withOppgaveApi(repository = repository) { + withOppgaveApi(journalpostRepository = repository) { client.get("v1/journalpost/$søknadId").let { response -> response.status shouldBe HttpStatusCode.OK response.bodyAsText() shouldBe journalpostId @@ -29,11 +29,11 @@ class JournalpostidApiTest { } private fun withOppgaveApi( - repository: Repository = mockk(relaxed = true), + journalpostRepository: JournalpostRepository = mockk(relaxed = true), test: suspend ApplicationTestBuilder.() -> Unit, ) { testApplication { - application { journalpostApi(repository) } + application { journalpostApi(journalpostRepository) } test() } } diff --git a/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/Postgres.kt b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/Postgres.kt new file mode 100644 index 0000000..95f404c --- /dev/null +++ b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/Postgres.kt @@ -0,0 +1,72 @@ +package no.nav.dagpenger.oppslag.journalpost.id + +import com.zaxxer.hikari.HikariDataSource +import org.flywaydb.core.internal.configuration.ConfigUtils +import org.testcontainers.containers.PostgreSQLContainer +import javax.sql.DataSource + +internal object Postgres { + val instance by lazy { + PostgreSQLContainer("postgres:15.5").apply { + start() + } + } + + fun withMigratedDb(block: (ds: DataSource) -> Unit) { + withCleanDb { + PostgresDataSourceBuilder.runMigration() + block(PostgresDataSourceBuilder.dataSource) + } + } + + fun withMigratedDb(): HikariDataSource { + setup() + PostgresDataSourceBuilder.runMigration() + return PostgresDataSourceBuilder.dataSource + } + + fun setup() { + System.setProperty(PostgresDataSourceBuilder.DB_HOST_KEY, instance.host) + System.setProperty( + PostgresDataSourceBuilder.DB_PORT_KEY, + instance.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT).toString(), + ) + System.setProperty(PostgresDataSourceBuilder.DB_DATABASE_KEY, instance.databaseName) + System.setProperty(PostgresDataSourceBuilder.DB_PASSWORD_KEY, instance.password) + System.setProperty(PostgresDataSourceBuilder.DB_USERNAME_KEY, instance.username) + } + + fun tearDown() { + System.clearProperty(PostgresDataSourceBuilder.DB_PASSWORD_KEY) + System.clearProperty(PostgresDataSourceBuilder.DB_USERNAME_KEY) + System.clearProperty(PostgresDataSourceBuilder.DB_HOST_KEY) + System.clearProperty(PostgresDataSourceBuilder.DB_PORT_KEY) + System.clearProperty(PostgresDataSourceBuilder.DB_DATABASE_KEY) + System.clearProperty(ConfigUtils.CLEAN_DISABLED) + } + + fun withCleanDb(block: () -> Unit) { + setup() + PostgresDataSourceBuilder.clean().run { + block() + }.also { + tearDown() + } + } + + fun withCleanDb( + target: String, + setup: () -> Unit, + test: () -> Unit, + ) { + this.setup() + PostgresDataSourceBuilder.clean().run { + PostgresDataSourceBuilder.runMigrationTo(target) + setup() + PostgresDataSourceBuilder.runMigration() + test() + }.also { + tearDown() + } + } +} diff --git a/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresJournalpostRepositoryTest.kt b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresJournalpostRepositoryTest.kt new file mode 100644 index 0000000..d9ce381 --- /dev/null +++ b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresJournalpostRepositoryTest.kt @@ -0,0 +1,18 @@ +package no.nav.dagpenger.oppslag.journalpost.id + +import io.kotest.matchers.shouldBe +import no.nav.dagpenger.oppslag.journalpost.id.Postgres.withMigratedDb +import org.junit.jupiter.api.Test +import java.util.UUID + +class PostgresJournalpostRepositoryTest { + @Test + fun `Skal kunne lagre og hente journalpostId basert på søknadId`() = + withMigratedDb { ds -> + val journalpostRepository = PostgresJournalpostRepository(dataSource = ds) + val søknadId = UUID.randomUUID() + val journalpostId = "123" + journalpostRepository.lagre(søknadId, journalpostId) + journalpostRepository.hent(søknadId) shouldBe journalpostId + } +} diff --git a/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresMigrationTest.kt b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresMigrationTest.kt new file mode 100644 index 0000000..de06ff4 --- /dev/null +++ b/src/test/kotlin/no/nav/dagpenger/oppslag/journalpost/id/PostgresMigrationTest.kt @@ -0,0 +1,16 @@ +package no.nav.dagpenger.oppslag.journalpost.id + +import io.kotest.matchers.shouldBe +import no.nav.dagpenger.oppslag.journalpost.id.Postgres.withCleanDb +import no.nav.dagpenger.oppslag.journalpost.id.PostgresDataSourceBuilder.runMigration +import org.junit.jupiter.api.Test + +class PostgresMigrationTest { + @Test + fun `Migration scripts are applied successfully`() { + withCleanDb { + val migrations = runMigration() + migrations shouldBe 1 + } + } +}