einige Ergänzungen

This commit is contained in:
2025-07-25 23:16:16 +02:00
parent 4c382e64a5
commit 7e0b56a247
70 changed files with 7795 additions and 1894 deletions
@@ -0,0 +1,60 @@
package at.mocode.horses.service.config
import at.mocode.horses.application.usecase.CreateHorseUseCase
import at.mocode.horses.application.usecase.TransactionalCreateHorseUseCase
import at.mocode.horses.application.usecase.UpdateHorseUseCase
import at.mocode.horses.application.usecase.DeleteHorseUseCase
import at.mocode.horses.application.usecase.GetHorseUseCase
import at.mocode.horses.domain.repository.HorseRepository
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
/**
* Application configuration for the Horses Service.
*
* This configuration wires the use cases as Spring beans.
*/
@Configuration
class ApplicationConfiguration {
/**
* Creates the CreateHorseUseCase as a Spring bean.
*/
@Bean
fun createHorseUseCase(horseRepository: HorseRepository): CreateHorseUseCase {
return CreateHorseUseCase(horseRepository)
}
/**
* Creates the TransactionalCreateHorseUseCase as a Spring bean.
* This version ensures all database operations run within a single transaction.
*/
@Bean
fun transactionalCreateHorseUseCase(horseRepository: HorseRepository): TransactionalCreateHorseUseCase {
return TransactionalCreateHorseUseCase(horseRepository)
}
/**
* Creates the UpdateHorseUseCase as a Spring bean.
*/
@Bean
fun updateHorseUseCase(horseRepository: HorseRepository): UpdateHorseUseCase {
return UpdateHorseUseCase(horseRepository)
}
/**
* Creates the DeleteHorseUseCase as a Spring bean.
*/
@Bean
fun deleteHorseUseCase(horseRepository: HorseRepository): DeleteHorseUseCase {
return DeleteHorseUseCase(horseRepository)
}
/**
* Creates the GetHorseUseCase as a Spring bean.
*/
@Bean
fun getHorseUseCase(horseRepository: HorseRepository): GetHorseUseCase {
return GetHorseUseCase(horseRepository)
}
}
@@ -0,0 +1,106 @@
package at.mocode.horses.service.config
import at.mocode.core.utils.database.DatabaseConfig
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.horses.infrastructure.persistence.HorseTable
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import org.slf4j.LoggerFactory
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
/**
* Database configuration for the Horses Service.
*
* This configuration ensures that Database.connect() is called properly
* before any Exposed operations are performed.
*/
@Configuration
@Profile("!test")
class HorsesDatabaseConfiguration {
private val log = LoggerFactory.getLogger(HorsesDatabaseConfiguration::class.java)
@PostConstruct
fun initializeDatabase() {
log.info("Initializing database schema for Horses Service...")
try {
// Database connection is already initialized by the gateway
// Only initialize the schema for this service
transaction {
SchemaUtils.createMissingTablesAndColumns(HorseTable)
log.info("Horse database schema initialized successfully")
}
} catch (e: Exception) {
log.error("Failed to initialize database schema", e)
throw e
}
}
@PreDestroy
fun closeDatabase() {
log.info("Closing database connection for Horses Service...")
try {
DatabaseFactory.close()
log.info("Database connection closed successfully")
} catch (e: Exception) {
log.error("Error closing database connection", e)
}
}
}
/**
* Test-specific database configuration.
*/
@Configuration
@Profile("test")
class HorsesTestDatabaseConfiguration {
private val log = LoggerFactory.getLogger(HorsesTestDatabaseConfiguration::class.java)
@PostConstruct
fun initializeTestDatabase() {
log.info("Initializing test database connection for Horses Service...")
try {
// Use H2 in-memory database for tests
val testConfig = DatabaseConfig(
jdbcUrl = "jdbc:h2:mem:horses_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
username = "sa",
password = "",
driverClassName = "org.h2.Driver",
maxPoolSize = 5,
minPoolSize = 1,
autoMigrate = true
)
DatabaseFactory.init(testConfig)
log.info("Test database connection initialized successfully")
// Initialize database schema for tests
transaction {
SchemaUtils.createMissingTablesAndColumns(HorseTable)
log.info("Test horse database schema initialized successfully")
}
} catch (e: Exception) {
log.error("Failed to initialize test database connection", e)
throw e
}
}
@PreDestroy
fun closeTestDatabase() {
log.info("Closing test database connection for Horses Service...")
try {
DatabaseFactory.close()
log.info("Test database connection closed successfully")
} catch (e: Exception) {
log.error("Error closing test database connection", e)
}
}
}
@@ -0,0 +1,171 @@
package at.mocode.horses.service.integration
import at.mocode.horses.domain.model.DomPferd
import at.mocode.horses.domain.repository.HorseRepository
import at.mocode.core.domain.model.PferdeGeschlechtE
import kotlinx.coroutines.*
import kotlinx.datetime.LocalDate
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestPropertySource
import org.springframework.beans.factory.annotation.Autowired
import kotlin.test.assertTrue
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
/**
* Integration tests to demonstrate and verify transaction context issues with coroutines.
*
* This test class reproduces the race condition that can occur when multiple
* coroutines perform database operations without proper transaction boundaries.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@TestPropertySource(properties = [
"spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
"spring.jpa.hibernate.ddl-auto=create-drop"
])
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TransactionContextTest {
@Autowired
private lateinit var horseRepository: HorseRepository
@BeforeEach
fun setUp() {
runBlocking {
// Clean up any existing test data
// Note: This is a simplified cleanup - in a real scenario you'd have proper cleanup
}
}
@Test
fun `should demonstrate race condition without transaction boundaries`(): Unit = runBlocking {
println("[DEBUG_LOG] Starting race condition test")
val lebensnummer = "TEST-RACE-001"
val chipNummer = "CHIP-RACE-001"
// Create two horses with the same identifiers
val horse1 = DomPferd(
pferdeName = "Race Horse 1",
geschlecht = PferdeGeschlechtE.WALLACH,
geburtsdatum = LocalDate(2020, 1, 1),
lebensnummer = lebensnummer,
chipNummer = chipNummer,
istAktiv = true
)
val horse2 = DomPferd(
pferdeName = "Race Horse 2",
geschlecht = PferdeGeschlechtE.STUTE,
geburtsdatum = LocalDate(2020, 1, 2),
lebensnummer = lebensnummer, // Same lebensnummer - should cause conflict
chipNummer = chipNummer, // Same chipNummer - should cause conflict
istAktiv = true
)
println("[DEBUG_LOG] Created horses with duplicate identifiers")
// Simulate the use case logic: check uniqueness then save
// This mimics what CreateHorseUseCase.execute() does without transactions
suspend fun createHorseWithChecks(horse: DomPferd): Boolean {
return try {
// Check uniqueness constraints (like in checkUniquenessConstraints)
val existsByLebensnummer = horse.lebensnummer?.let {
horseRepository.existsByLebensnummer(it)
} ?: false
val existsByChipNummer = horse.chipNummer?.let {
horseRepository.existsByChipNummer(it)
} ?: false
println("[DEBUG_LOG] ${horse.pferdeName}: existsByLebensnummer=$existsByLebensnummer, existsByChipNummer=$existsByChipNummer")
if (existsByLebensnummer || existsByChipNummer) {
println("[DEBUG_LOG] ${horse.pferdeName}: Uniqueness check failed")
false
} else {
// Save the horse (like in the use case)
horseRepository.save(horse)
println("[DEBUG_LOG] ${horse.pferdeName}: Saved successfully")
true
}
} catch (e: Exception) {
println("[DEBUG_LOG] ${horse.pferdeName}: Exception during creation: ${e.message}")
false
}
}
// Launch two concurrent coroutines to create horses
val results = listOf(
async {
println("[DEBUG_LOG] Starting creation 1")
createHorseWithChecks(horse1)
},
async {
println("[DEBUG_LOG] Starting creation 2")
createHorseWithChecks(horse2)
}
).awaitAll()
println("[DEBUG_LOG] Both operations completed")
println("[DEBUG_LOG] Result 1 success: ${results[0]}")
println("[DEBUG_LOG] Result 2 success: ${results[1]}")
// In a properly transactional system, exactly one should succeed
val successCount = results.count { it }
val failureCount = results.count { !it }
println("[DEBUG_LOG] Success count: $successCount, Failure count: $failureCount")
// Check what actually got saved in the database
val savedByLebensnummer = horseRepository.findByLebensnummer(lebensnummer)
val savedByChipNummer = horseRepository.findByChipNummer(chipNummer)
println("[DEBUG_LOG] Found by lebensnummer: ${savedByLebensnummer?.pferdeName}")
println("[DEBUG_LOG] Found by chipNummer: ${savedByChipNummer?.pferdeName}")
// This test demonstrates the issue - without transactions, both operations might succeed
// due to race conditions, or the behavior might be unpredictable
// The fix should ensure exactly one succeeds and one fails with a proper error
assertTrue(successCount >= 1, "At least one operation should succeed")
}
@Test
fun `should demonstrate transaction context propagation issue`(): Unit = runBlocking {
println("[DEBUG_LOG] Starting transaction context propagation test")
// This test will show that without @Transactional, each repository call
// runs in its own transaction context, which can lead to inconsistencies
val horse = DomPferd(
pferdeName = "Transaction Test Horse",
geschlecht = PferdeGeschlechtE.HENGST,
lebensnummer = "TRANS-TEST-001",
istAktiv = true
)
println("[DEBUG_LOG] Creating horse with repository operations")
// Simulate multiple repository operations that should be atomic
val existsCheck = horseRepository.existsByLebensnummer("TRANS-TEST-001")
println("[DEBUG_LOG] Exists check result: $existsCheck")
if (!existsCheck) {
val savedHorse = horseRepository.save(horse)
println("[DEBUG_LOG] Horse saved successfully: ${savedHorse.pferdeName}")
assertNotNull(savedHorse)
assertEquals("Transaction Test Horse", savedHorse.pferdeName)
}
// The issue is that without @Transactional, if an exception occurs after
// the uniqueness checks but before/during save, the database state
// might be inconsistent
val finalCheck = horseRepository.findByLebensnummer("TRANS-TEST-001")
assertNotNull(finalCheck, "Horse should be saved in database")
}
}
@@ -0,0 +1,186 @@
package at.mocode.horses.service.integration
import at.mocode.horses.application.usecase.TransactionalCreateHorseUseCase
import at.mocode.horses.domain.repository.HorseRepository
import at.mocode.core.domain.model.PferdeGeschlechtE
import com.benasher44.uuid.uuid4
import kotlinx.coroutines.*
import kotlinx.datetime.LocalDate
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestPropertySource
import org.springframework.beans.factory.annotation.Autowired
import kotlin.test.assertTrue
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
/**
* Integration tests to verify that transaction context issues with coroutines are resolved.
*
* This test class verifies that the transactional use cases properly handle
* concurrent operations and maintain data consistency.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@TestPropertySource(properties = [
"spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
"spring.jpa.hibernate.ddl-auto=create-drop"
])
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TransactionalContextTest {
@Autowired
private lateinit var horseRepository: HorseRepository
@Autowired
private lateinit var transactionalCreateHorseUseCase: TransactionalCreateHorseUseCase
@BeforeEach
fun setUp() {
runBlocking {
// Clean up any existing test data
// Note: This is a simplified cleanup - in a real scenario you'd have proper cleanup
}
}
@Test
fun `should handle race condition properly with transaction boundaries`(): Unit = runBlocking {
println("[DEBUG_LOG] Starting transactional race condition test")
val lebensnummer = "TRANS-RACE-001"
val chipNummer = "TRANS-CHIP-001"
// Create two identical horse creation requests
val ownerId = uuid4()
val request1 = TransactionalCreateHorseUseCase.CreateHorseRequest(
pferdeName = "Transactional Race Horse 1",
geschlecht = PferdeGeschlechtE.WALLACH,
geburtsdatum = LocalDate(2020, 1, 1),
lebensnummer = lebensnummer,
chipNummer = chipNummer,
besitzerId = ownerId
)
val request2 = TransactionalCreateHorseUseCase.CreateHorseRequest(
pferdeName = "Transactional Race Horse 2",
geschlecht = PferdeGeschlechtE.STUTE,
geburtsdatum = LocalDate(2020, 1, 2),
lebensnummer = lebensnummer, // Same lebensnummer - should cause conflict
chipNummer = chipNummer, // Same chipNummer - should cause conflict
besitzerId = ownerId
)
println("[DEBUG_LOG] Created requests with duplicate identifiers")
// Launch two concurrent coroutines to create horses using transactional use case
val results = listOf(
async {
println("[DEBUG_LOG] Starting transactional creation 1")
transactionalCreateHorseUseCase.execute(request1)
},
async {
println("[DEBUG_LOG] Starting transactional creation 2")
transactionalCreateHorseUseCase.execute(request2)
}
).awaitAll()
println("[DEBUG_LOG] Both transactional operations completed")
println("[DEBUG_LOG] Result 1 success: ${results[0].success}")
println("[DEBUG_LOG] Result 2 success: ${results[1].success}")
// With proper transaction boundaries, exactly one should succeed
val successCount = results.count { it.success }
val failureCount = results.count { !it.success }
println("[DEBUG_LOG] Success count: $successCount, Failure count: $failureCount")
// Verify that exactly one operation succeeded and one failed
assertEquals(1, successCount, "Exactly one operation should succeed with proper transactions")
assertEquals(1, failureCount, "Exactly one operation should fail with proper transactions")
// Check what actually got saved in the database
val savedByLebensnummer = horseRepository.findByLebensnummer(lebensnummer)
val savedByChipNummer = horseRepository.findByChipNummer(chipNummer)
println("[DEBUG_LOG] Found by lebensnummer: ${savedByLebensnummer?.pferdeName}")
println("[DEBUG_LOG] Found by chipNummer: ${savedByChipNummer?.pferdeName}")
// Verify that exactly one horse was saved
assertNotNull(savedByLebensnummer, "One horse should be saved with the lebensnummer")
assertNotNull(savedByChipNummer, "One horse should be saved with the chipNummer")
assertEquals(savedByLebensnummer?.pferdId, savedByChipNummer?.pferdId, "Both queries should return the same horse")
// Verify that the failed operation returned proper error
val failedResult = results.find { !it.success }
assertNotNull(failedResult, "There should be one failed result")
assertEquals("UNIQUENESS_ERROR", failedResult?.error?.code, "Failed operation should return uniqueness error")
println("[DEBUG_LOG] Transactional test completed successfully - race condition properly handled")
}
@Test
fun `should maintain transaction consistency on validation failure`(): Unit = runBlocking {
println("[DEBUG_LOG] Starting transaction consistency test")
// Create a request with invalid data that will fail validation
val request = TransactionalCreateHorseUseCase.CreateHorseRequest(
pferdeName = "", // Empty name should fail validation
geschlecht = PferdeGeschlechtE.HENGST,
lebensnummer = "VALIDATION-TEST-001",
stockmass = 300, // Invalid height should fail validation
besitzerId = uuid4() // Add owner to pass basic validation
)
println("[DEBUG_LOG] Executing transactional create with invalid data")
val result = transactionalCreateHorseUseCase.execute(request)
println("[DEBUG_LOG] Creation result: success=${result.success}")
// Verify that the operation failed due to validation
assertTrue(!result.success, "Operation should fail due to validation errors")
assertEquals("VALIDATION_ERROR", result.error?.code, "Should return validation error")
// Verify that no horse was saved in the database
val savedHorse = horseRepository.findByLebensnummer("VALIDATION-TEST-001")
assertTrue(savedHorse == null, "No horse should be saved when validation fails")
println("[DEBUG_LOG] Transaction consistency test completed - no data saved on validation failure")
}
@Test
fun `should successfully create horse with valid data in transaction`(): Unit = runBlocking {
println("[DEBUG_LOG] Starting successful transactional creation test")
val request = TransactionalCreateHorseUseCase.CreateHorseRequest(
pferdeName = "Successful Transaction Horse",
geschlecht = PferdeGeschlechtE.STUTE,
geburtsdatum = LocalDate(2021, 6, 15),
lebensnummer = "SUCCESS-TEST-001",
chipNummer = "SUCCESS-CHIP-001",
rasse = "Warmblut",
stockmass = 165,
besitzerId = uuid4() // Add required owner
)
println("[DEBUG_LOG] Executing transactional create with valid data")
val result = transactionalCreateHorseUseCase.execute(request)
println("[DEBUG_LOG] Creation result: success=${result.success}")
// Verify that the operation succeeded
assertTrue(result.success, "Operation should succeed with valid data")
assertNotNull(result.data, "Result should contain the created horse")
assertEquals("Successful Transaction Horse", result.data?.pferdeName, "Horse name should match")
// Verify that the horse was saved in the database
val savedHorse = horseRepository.findByLebensnummer("SUCCESS-TEST-001")
assertNotNull(savedHorse, "Horse should be saved in database")
assertEquals("Successful Transaction Horse", savedHorse.pferdeName, "Saved horse name should match")
assertEquals("SUCCESS-CHIP-001", savedHorse.chipNummer, "Saved horse chip number should match")
println("[DEBUG_LOG] Successful transactional creation test completed")
}
}