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