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
Timo Bryant 2024-05-03 19:46:46 +02:00
parent 8a9732eff9
commit fd47bc3baa
7 changed files with 217 additions and 31 deletions

View File

@ -26,9 +26,9 @@ testing {
} }
dependencies { dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("com.willowtreeapps.assertk:assertk:0.28.1") testImplementation("com.willowtreeapps.assertk:assertk:0.28.1")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation(platform("org.junit:junit-bom:5.10.2"))
} }

View File

@ -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-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.5.0"
kotlinx-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" 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-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-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"} ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor"}
@ -81,6 +82,10 @@ kotlinx = [
"kotlinx-json", "kotlinx-json",
] ]
kotlinx-test = [
"kotlinx-coroutines-test"
]
logging = [ logging = [
"slf4j-api", "slf4j-api",
"logback-classic", "logback-classic",

View File

@ -2,4 +2,6 @@ dependencies {
implementation(project(":modules:ModuleCore")) implementation(project(":modules:ModuleCore"))
implementation("org.postgresql:postgresql:42.7.3") implementation("org.postgresql:postgresql:42.7.3")
implementation(libs.bundles.exposed) implementation(libs.bundles.exposed)
implementation(libs.bundles.kotlinx.test)
testImplementation("com.appmattus.fixture:fixture:1.2.0")
} }

View File

@ -1,9 +1,13 @@
package de.itkl.modCredentialManager package de.itkl.modCredentialManager
import kotlinx.coroutines.flow.Flow
interface CredentialManager { interface CredentialManager {
fun find(id: String): Credential? suspend fun search(searchTerm: String): Flow<Credential>
fun add(credential: Credential) suspend fun find(id: String): Credential?
fun delete(id: String) suspend fun add(credential: Credential)
suspend fun delete(id: String)
} }

View File

@ -1,8 +1,12 @@
package de.itkl.modCredentialManager.postgresBackend package de.itkl.modCredentialManager.postgresBackend
import de.itkl.modCredentialManager.Credential import de.itkl.modCredentialManager.*
import de.itkl.modCredentialManager.CredentialManager
import io.github.oshai.kotlinlogging.KotlinLogging 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.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.jetbrains.exposed.dao.UUIDEntity 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.*
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.sql.Connection
import java.util.UUID import java.util.UUID
import kotlin.sequences.Sequence
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
private val Log = KotlinLogging.logger { } private val Log = KotlinLogging.logger { }
class PostgresBackend : CredentialManager { class PostgresBackend(config: Config) : CredentialManager {
val dbConnection = Database.connect( data class Config(
"jdbc:postgresql://localhost:5432/postgres", val user: String,
driver = "org.postgresql.Driver", val url: JdbcUrl,
user = "kinch", ) {
fun toConnection(): Database {
return Database.connect(
url = url.toString(),
driver = url.driver,
user = user,
) )
}
}
sealed interface JdbcUrl {
val driver: String
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"
}
}
}
private val dbConnection = config.toConnection()
private suspend inline fun <T> 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 { init {
runBlocking {
transaction { transaction {
addLogger(StdOutSqlLogger) addLogger(StdOutSqlLogger)
SchemaUtils.create(Credentials) SchemaUtils.create(Credentials)
} }
} }
}
override fun find(id: String): Credential? { override suspend fun search(searchTerm: String): Flow<Credential> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun add(credential: Credential) { override suspend fun find(id: String): Credential? {
transaction { TODO("Not yet implemented")
}
suspend fun listInsert(credentials: Sequence<Credential>) {
transaction(writable = true) {
credentials.forEach {
insert(it)
}
}
}
suspend fun batchInsert(credentials: Sequence<Credential>) {
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 { DbCredential.new {
displayName = credential.id displayName = credential.id
secret = "timo" secret = credential.password
username = credential.username
notes = credential.notes
expiresAt = Clock.System.now() + 1.hours expiresAt = Clock.System.now() + 1.hours
} }
} }
} }
}
override fun delete(id: String) { override suspend fun delete(id: String) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View File

@ -1,12 +1,73 @@
package de.itkl.modCredentialManager.postgresBackend package de.itkl.modCredentialManager.postgresBackend
import com.appmattus.kotlinfixture.kotlinFixture
import de.itkl.modCredentialManager.UsernameAndPassword import de.itkl.modCredentialManager.UsernameAndPassword
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.* 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.Test
import org.junit.jupiter.api.TestMethodOrder
@TestMethodOrder(
MethodOrderer.OrderAnnotation::class,
)
class PostgresBackendTest { class PostgresBackendTest {
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<UsernameAndPassword> {
return (1..10_000)
.asSequence()
.map {
fixture<UsernameAndPassword>()
}
}
@Order(1)
@Test @Test
fun `can create a table`() { fun `dumb insert`() =
PostgresBackend().add(UsernameAndPassword("username", "password", username = "", password = "")) 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())
} }
} }

View File

@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>