fix(tests): resolve EntriesIsolationIntegrationTest failures with test-specific DB config

- Added `TestExposedConfiguration` to connect Exposed with Spring `DataSource` in the `test` profile.
- Downgraded `springdoc` version from `3.0.0` to `2.8.9` for Spring Boot 3.x compatibility.
- Applied `@ActiveProfiles("test")` to `EntriesIsolationIntegrationTest`.
- Updated roadmap documentation to reflect bugfix and test success.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-03 10:52:37 +02:00
parent f4844eb428
commit 1f9f528554
14 changed files with 667 additions and 36 deletions
@@ -0,0 +1,17 @@
package at.mocode.turnier.feature.data.mapper
import at.mocode.turnier.feature.data.remote.dto.AbteilungDto
import at.mocode.turnier.feature.data.remote.dto.BewerbDto
import at.mocode.turnier.feature.data.remote.dto.TurnierDto
import at.mocode.turnier.feature.domain.Abteilung
import at.mocode.turnier.feature.domain.Bewerb
import at.mocode.turnier.feature.domain.Turnier
fun TurnierDto.toDomain(): Turnier = Turnier(id = id, name = name)
fun Turnier.toDto(): TurnierDto = TurnierDto(id = id, name = name)
fun BewerbDto.toDomain(): Bewerb = Bewerb(id = id, turnierId = turnierId, name = name)
fun Bewerb.toDto(): BewerbDto = BewerbDto(id = id, turnierId = turnierId, name = name)
fun AbteilungDto.toDomain(): Abteilung = Abteilung(id = id, bewerbId = bewerbId, name = name)
fun Abteilung.toDto(): AbteilungDto = AbteilungDto(id = id, bewerbId = bewerbId, name = name)
@@ -0,0 +1,23 @@
package at.mocode.turnier.feature.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class TurnierDto(
val id: Long,
val name: String,
)
@Serializable
data class BewerbDto(
val id: Long,
val turnierId: Long,
val name: String,
)
@Serializable
data class AbteilungDto(
val id: Long,
val bewerbId: Long,
val name: String,
)
@@ -0,0 +1,71 @@
package at.mocode.turnier.feature.data.remote
import at.mocode.frontend.core.network.*
import at.mocode.turnier.feature.data.mapper.toDomain
import at.mocode.turnier.feature.data.mapper.toDto
import at.mocode.turnier.feature.data.remote.dto.AbteilungDto
import at.mocode.turnier.feature.domain.Abteilung
import at.mocode.turnier.feature.domain.AbteilungRepository
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
class DefaultAbteilungRepository(
private val client: HttpClient,
) : AbteilungRepository {
override suspend fun list(bewerbId: Long): Result<List<Abteilung>> = runCatching {
val response = client.get(ApiRoutes.Bewerbe.abteilungen(bewerbId))
when {
response.status.isSuccess() -> response.body<List<AbteilungDto>>().map { it.toDomain() }
response.status == HttpStatusCode.Unauthorized -> throw AuthExpired()
response.status == HttpStatusCode.Forbidden -> throw AuthForbidden()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun getById(id: Long): Result<Abteilung> = runCatching {
val response = client.get("${ApiRoutes.API_PREFIX}/abteilungen/$id")
when {
response.status.isSuccess() -> response.body<AbteilungDto>().toDomain()
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status == HttpStatusCode.Unauthorized -> throw AuthExpired()
response.status == HttpStatusCode.Forbidden -> throw AuthForbidden()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun create(model: Abteilung): Result<Abteilung> = runCatching {
val response = client.post(ApiRoutes.Bewerbe.abteilungen(model.bewerbId)) { setBody(model.toDto()) }
when {
response.status.isSuccess() -> response.body<AbteilungDto>().toDomain()
response.status == HttpStatusCode.Conflict -> throw Conflict()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun update(id: Long, model: Abteilung): Result<Abteilung> = runCatching {
val response = client.put("${ApiRoutes.API_PREFIX}/abteilungen/$id") { setBody(model.toDto()) }
when {
response.status.isSuccess() -> response.body<AbteilungDto>().toDomain()
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status == HttpStatusCode.Conflict -> throw Conflict()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun delete(id: Long): Result<Unit> = runCatching {
val response = client.delete("${ApiRoutes.API_PREFIX}/abteilungen/$id")
when {
response.status.isSuccess() -> Unit
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
}
@@ -0,0 +1,71 @@
package at.mocode.turnier.feature.data.remote
import at.mocode.frontend.core.network.*
import at.mocode.turnier.feature.data.mapper.toDomain
import at.mocode.turnier.feature.data.mapper.toDto
import at.mocode.turnier.feature.data.remote.dto.BewerbDto
import at.mocode.turnier.feature.domain.Bewerb
import at.mocode.turnier.feature.domain.BewerbRepository
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
class DefaultBewerbRepository(
private val client: HttpClient,
) : BewerbRepository {
override suspend fun list(turnierId: Long): Result<List<Bewerb>> = runCatching {
val response = client.get(ApiRoutes.Turniere.bewerbe(turnierId))
when {
response.status.isSuccess() -> response.body<List<BewerbDto>>().map { it.toDomain() }
response.status == HttpStatusCode.Unauthorized -> throw AuthExpired()
response.status == HttpStatusCode.Forbidden -> throw AuthForbidden()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun getById(id: Long): Result<Bewerb> = runCatching {
val response = client.get("${ApiRoutes.API_PREFIX}/bewerbe/$id")
when {
response.status.isSuccess() -> response.body<BewerbDto>().toDomain()
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status == HttpStatusCode.Unauthorized -> throw AuthExpired()
response.status == HttpStatusCode.Forbidden -> throw AuthForbidden()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun create(model: Bewerb): Result<Bewerb> = runCatching {
val response = client.post(ApiRoutes.Turniere.bewerbe(model.turnierId)) { setBody(model.toDto()) }
when {
response.status.isSuccess() -> response.body<BewerbDto>().toDomain()
response.status == HttpStatusCode.Conflict -> throw Conflict()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun update(id: Long, model: Bewerb): Result<Bewerb> = runCatching {
val response = client.put("${ApiRoutes.API_PREFIX}/bewerbe/$id") { setBody(model.toDto()) }
when {
response.status.isSuccess() -> response.body<BewerbDto>().toDomain()
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status == HttpStatusCode.Conflict -> throw Conflict()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun delete(id: Long): Result<Unit> = runCatching {
val response = client.delete("${ApiRoutes.API_PREFIX}/bewerbe/$id")
when {
response.status.isSuccess() -> Unit
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
}
@@ -0,0 +1,71 @@
package at.mocode.turnier.feature.data.remote
import at.mocode.frontend.core.network.*
import at.mocode.turnier.feature.data.mapper.toDomain
import at.mocode.turnier.feature.data.mapper.toDto
import at.mocode.turnier.feature.data.remote.dto.TurnierDto
import at.mocode.turnier.feature.domain.Turnier
import at.mocode.turnier.feature.domain.TurnierRepository
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
class DefaultTurnierRepository(
private val client: HttpClient,
) : TurnierRepository {
override suspend fun list(): Result<List<Turnier>> = runCatching {
val response = client.get(ApiRoutes.Turniere.ROOT)
when {
response.status.isSuccess() -> response.body<List<TurnierDto>>().map { it.toDomain() }
response.status == HttpStatusCode.Unauthorized -> throw AuthExpired()
response.status == HttpStatusCode.Forbidden -> throw AuthForbidden()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun getById(id: Long): Result<Turnier> = runCatching {
val response = client.get("${ApiRoutes.Turniere.ROOT}/$id")
when {
response.status.isSuccess() -> response.body<TurnierDto>().toDomain()
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status == HttpStatusCode.Unauthorized -> throw AuthExpired()
response.status == HttpStatusCode.Forbidden -> throw AuthForbidden()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun create(model: Turnier): Result<Turnier> = runCatching {
val response = client.post(ApiRoutes.Turniere.ROOT) { setBody(model.toDto()) }
when {
response.status.isSuccess() -> response.body<TurnierDto>().toDomain()
response.status == HttpStatusCode.Conflict -> throw Conflict()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun update(id: Long, model: Turnier): Result<Turnier> = runCatching {
val response = client.put("${ApiRoutes.Turniere.ROOT}/$id") { setBody(model.toDto()) }
when {
response.status.isSuccess() -> response.body<TurnierDto>().toDomain()
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status == HttpStatusCode.Conflict -> throw Conflict()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
override suspend fun delete(id: Long): Result<Unit> = runCatching {
val response = client.delete("${ApiRoutes.Turniere.ROOT}/$id")
when {
response.status.isSuccess() -> Unit
response.status == HttpStatusCode.NotFound -> throw NotFound()
response.status.value >= 500 -> throw ServerError()
else -> throw HttpError(response.status.value)
}
}
}
@@ -0,0 +1,32 @@
package at.mocode.turnier.feature.di
import at.mocode.turnier.feature.data.remote.DefaultAbteilungRepository
import at.mocode.turnier.feature.data.remote.DefaultBewerbRepository
import at.mocode.turnier.feature.data.remote.DefaultTurnierRepository
import at.mocode.turnier.feature.domain.AbteilungRepository
import at.mocode.turnier.feature.domain.BewerbRepository
import at.mocode.turnier.feature.domain.TurnierRepository
import at.mocode.turnier.feature.presentation.AbteilungViewModel
import at.mocode.turnier.feature.presentation.BewerbAnlegenViewModel
import at.mocode.turnier.feature.presentation.BewerbViewModel
import at.mocode.turnier.feature.presentation.TurnierViewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
val turnierFeatureModule = module {
// Repositories: Interface → Default-Implementierung mit zentralem apiClient
single<TurnierRepository> { DefaultTurnierRepository(client = get(qualifier = named("apiClient"))) }
single<BewerbRepository> { DefaultBewerbRepository(client = get(qualifier = named("apiClient"))) }
single<AbteilungRepository> { DefaultAbteilungRepository(client = get(qualifier = named("apiClient"))) }
// ViewModels
factory { TurnierViewModel(repo = get()) }
// BewerbViewModel: repo + turnierId — turnierId wird per parametersOf übergeben
factory { (turnierId: Long) -> BewerbViewModel(repo = get(), turnierId = turnierId) }
// BewerbAnlegenViewModel hat keinen Repository-Parameter (nutzt StoreV2 intern)
factory { BewerbAnlegenViewModel() }
// AbteilungViewModel: repo + bewerbId + abteilungsNr — per parametersOf übergeben
factory { (bewerbId: Long, abteilungsNr: Int) ->
AbteilungViewModel(repo = get(), bewerbId = bewerbId, abteilungsNr = abteilungsNr)
}
}