From fd47bc3baa0a0a9f47c989444142d4ac0d1f5e8d Mon Sep 17 00:00:00 2001 From: Timo Bryant Date: Fri, 3 May 2024 19:46:46 +0200 Subject: [PATCH] Update PostgresBackend with configurable connections and test implementations The PostgresBackend class has been updated to allow for more configurable database connection methods. This includes more granular control over specific connection properties and the introduction of a sealed class constructor for creating distinct JDBC connection URLs. Also, test implementations for credential manager and Postgres backend have been updated, particularly to handle coroutine suspension. Furthermore, dependencies for test modules and kotlinx coroutines have been added to the build scripts. --- .../main/kotlin/module-convention.gradle.kts | 4 +- gradle/libs.versions.toml | 5 + modules/ModCredentialManager/build.gradle.kts | 2 + .../modCredentialManager/CredentialManager.kt | 10 +- .../postgresBackend/PostgresBackend.kt | 149 +++++++++++++++--- .../postgresBackend/PostgresBackendTest.kt | 67 +++++++- .../src/test/resources/logback.xml | 11 ++ 7 files changed, 217 insertions(+), 31 deletions(-) create mode 100644 modules/ModCredentialManager/src/test/resources/logback.xml diff --git a/buildSrc/src/main/kotlin/module-convention.gradle.kts b/buildSrc/src/main/kotlin/module-convention.gradle.kts index cd24c10..e3b3ef0 100644 --- a/buildSrc/src/main/kotlin/module-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/module-convention.gradle.kts @@ -26,9 +26,9 @@ testing { } dependencies { - testImplementation(platform("org.junit:junit-bom:5.10.2")) - testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation("com.willowtreeapps.assertk:assertk:0.28.1") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(platform("org.junit:junit-bom:5.10.2")) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a7316a6..0298ecb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ kotlin-logging = {module = "io.github.oshai:kotlin-logging-jvm", version.ref = " kotlinx-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.5.0" kotlinx-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx" } ktor-server-content-negotation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor"} @@ -81,6 +82,10 @@ kotlinx = [ "kotlinx-json", ] +kotlinx-test = [ + "kotlinx-coroutines-test" +] + logging = [ "slf4j-api", "logback-classic", diff --git a/modules/ModCredentialManager/build.gradle.kts b/modules/ModCredentialManager/build.gradle.kts index aadd0f3..ac2aa64 100644 --- a/modules/ModCredentialManager/build.gradle.kts +++ b/modules/ModCredentialManager/build.gradle.kts @@ -2,4 +2,6 @@ dependencies { implementation(project(":modules:ModuleCore")) implementation("org.postgresql:postgresql:42.7.3") implementation(libs.bundles.exposed) + implementation(libs.bundles.kotlinx.test) + testImplementation("com.appmattus.fixture:fixture:1.2.0") } diff --git a/modules/ModCredentialManager/src/main/kotlin/de/itkl/modCredentialManager/CredentialManager.kt b/modules/ModCredentialManager/src/main/kotlin/de/itkl/modCredentialManager/CredentialManager.kt index f96b921..08b1260 100644 --- a/modules/ModCredentialManager/src/main/kotlin/de/itkl/modCredentialManager/CredentialManager.kt +++ b/modules/ModCredentialManager/src/main/kotlin/de/itkl/modCredentialManager/CredentialManager.kt @@ -1,9 +1,13 @@ package de.itkl.modCredentialManager +import kotlinx.coroutines.flow.Flow + interface CredentialManager { - fun find(id: String): Credential? + suspend fun search(searchTerm: String): Flow - fun add(credential: Credential) + suspend fun find(id: String): Credential? - fun delete(id: String) + suspend fun add(credential: Credential) + + suspend fun delete(id: String) } diff --git a/modules/ModCredentialManager/src/main/kotlin/de/itkl/modCredentialManager/postgresBackend/PostgresBackend.kt b/modules/ModCredentialManager/src/main/kotlin/de/itkl/modCredentialManager/postgresBackend/PostgresBackend.kt index e2b6c3a..3f7cdbe 100644 --- a/modules/ModCredentialManager/src/main/kotlin/de/itkl/modCredentialManager/postgresBackend/PostgresBackend.kt +++ b/modules/ModCredentialManager/src/main/kotlin/de/itkl/modCredentialManager/postgresBackend/PostgresBackend.kt @@ -1,8 +1,12 @@ package de.itkl.modCredentialManager.postgresBackend -import de.itkl.modCredentialManager.Credential -import de.itkl.modCredentialManager.CredentialManager +import de.itkl.modCredentialManager.* import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.jetbrains.exposed.dao.UUIDEntity @@ -12,40 +16,139 @@ import org.jetbrains.exposed.dao.id.UUIDTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.kotlin.datetime.timestamp import org.jetbrains.exposed.sql.transactions.transaction +import java.sql.Connection import java.util.UUID +import kotlin.sequences.Sequence import kotlin.time.Duration.Companion.hours private val Log = KotlinLogging.logger { } -class PostgresBackend : CredentialManager { - val dbConnection = Database.connect( - "jdbc:postgresql://localhost:5432/postgres", - driver = "org.postgresql.Driver", - user = "kinch", - ) - - init { - transaction { - addLogger(StdOutSqlLogger) - SchemaUtils.create(Credentials) +class PostgresBackend(config: Config) : CredentialManager { + data class Config( + val user: String, + val url: JdbcUrl, + ) { + fun toConnection(): Database { + return Database.connect( + url = url.toString(), + driver = url.driver, + user = user, + ) } } - override fun find(id: String): Credential? { - TODO("Not yet implemented") - } + sealed interface JdbcUrl { + val driver: String - override fun add(credential: Credential) { - transaction { - DbCredential.new { - displayName = credential.id - secret = "timo" - expiresAt = Clock.System.now() + 1.hours + data class PostgreSql( + val host: String, + val port: Int, + val databaseName: String, + val batchInsert: Boolean = false, + ) : JdbcUrl { + override val driver: String + get() = "org.postgresql.Driver" + + override fun toString(): String { + return "jdbc:postgresql://$host:$port/$databaseName?reWriteBatchedInserts=$batchInsert" } } } - override fun delete(id: String) { + private val dbConnection = config.toConnection() + + private suspend inline fun transaction( + writable: Boolean = false, + noinline op: Transaction.() -> T, + ): T = + coroutineScope { + withContext(Dispatchers.IO) { + transaction( + transactionIsolation = Connection.TRANSACTION_READ_UNCOMMITTED, + readOnly = !writable, + db = dbConnection, + op, + ) + } + } + + init { + runBlocking { + transaction { + addLogger(StdOutSqlLogger) + SchemaUtils.create(Credentials) + } + } + } + + override suspend fun search(searchTerm: String): Flow { + TODO("Not yet implemented") + } + + override suspend fun find(id: String): Credential? { + TODO("Not yet implemented") + } + + suspend fun listInsert(credentials: Sequence) { + transaction(writable = true) { + credentials.forEach { + insert(it) + } + } + } + + suspend fun batchInsert(credentials: Sequence) { + transaction(writable = true) { + Credentials.batchInsert( + credentials, + ignore = true, + shouldReturnGeneratedValues = false, + ) { cred -> + when (cred) { + is BearerToken -> { + this[Credentials.displayName] = cred.id + // fill other fields here ... + } + is RefreshToken -> { + this[Credentials.displayName] = cred.id + // fill other fields here ... + } + is UsernameAndPassword -> { + this[Credentials.displayName] = cred.id + this[Credentials.username] = cred.username + this[Credentials.secret] = cred.password + this[Credentials.notes] = cred.notes + this[Credentials.expiresAt] = Clock.System.now() + 1.hours + } + // handle other credential types here ... + } + } + } + } + + override suspend fun add(credential: Credential) { + transaction(writable = true) { + insert(credential) + } + } + + private fun insert(credential: Credential) { + when (credential) { + is BearerToken -> TODO() + is RefreshToken -> TODO() + is UsernameAndPassword -> { + DbCredential.new { + displayName = credential.id + secret = credential.password + username = credential.username + notes = credential.notes + expiresAt = Clock.System.now() + 1.hours + } + } + } + } + + override suspend fun delete(id: String) { TODO("Not yet implemented") } } diff --git a/modules/ModCredentialManager/src/test/kotlin/de/itkl/modCredentialManager/postgresBackend/PostgresBackendTest.kt b/modules/ModCredentialManager/src/test/kotlin/de/itkl/modCredentialManager/postgresBackend/PostgresBackendTest.kt index 21502e6..6b77bc0 100644 --- a/modules/ModCredentialManager/src/test/kotlin/de/itkl/modCredentialManager/postgresBackend/PostgresBackendTest.kt +++ b/modules/ModCredentialManager/src/test/kotlin/de/itkl/modCredentialManager/postgresBackend/PostgresBackendTest.kt @@ -1,12 +1,73 @@ package de.itkl.modCredentialManager.postgresBackend +import com.appmattus.kotlinfixture.kotlinFixture import de.itkl.modCredentialManager.UsernameAndPassword +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +@TestMethodOrder( + MethodOrderer.OrderAnnotation::class, +) class PostgresBackendTest { - @Test - fun `can create a table`() { - PostgresBackend().add(UsernameAndPassword("username", "password", username = "", password = "")) + val fixture = kotlinFixture() + private val db = PostgresBackend( + PostgresBackend.Config( + user = "tbr", + url = PostgresBackend.JdbcUrl.PostgreSql( + host = "localhost", + port = 5432, + databaseName = "postgres", + batchInsert = true, + ), + ), + ) + + private fun randomCredentials(): Sequence { + return (1..10_000) + .asSequence() + .map { + fixture() + } } + + @Order(1) + @Test + fun `dumb insert`() = + runTest { + randomCredentials().forEach { db.add(it) } + } + +// @Disabled +// @Order(2) +// @Test +// fun `async insert`() = +// runTest { +// val gate = Semaphore(10) +// randomCredentials().map { +// async { +// gate.withPermit { +// db.add(it) +// } +// } +// } +// .awaitAll() +// } + + @Order(3) + @Test + fun `single transaction`() = + runTest { + db.listInsert(randomCredentials()) + } + + @Order(4) + @Test + fun `batch insert`() = + runTest { + db.batchInsert(randomCredentials()) + } } diff --git a/modules/ModCredentialManager/src/test/resources/logback.xml b/modules/ModCredentialManager/src/test/resources/logback.xml new file mode 100644 index 0000000..6156c21 --- /dev/null +++ b/modules/ModCredentialManager/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file