fixing web-app

This commit is contained in:
stefan
2025-09-24 14:21:57 +02:00
parent cd2b0796a6
commit 1c4184809a
156 changed files with 440 additions and 1708 deletions
@@ -0,0 +1,26 @@
package at.mocode.horses.service
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
/**
* Main application class for the Horses Service.
*
* This service provides APIs for managing horses and their data.
*/
@SpringBootApplication
@ComponentScan(basePackages = [
"at.mocode.horses.service",
"at.mocode.horses.api",
"at.mocode.horses.infrastructure",
"at.mocode.infrastructure.messaging"
])
class HorsesServiceApplication
/**
* Main entry point for the Horses Service application.
*/
fun main(args: Array<String>) {
runApplication<HorsesServiceApplication>(*args)
}
@@ -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,350 @@
package at.mocode.horses.service.integration
import at.mocode.horses.domain.model.DomPferd
import at.mocode.horses.domain.repository.HorseRepository
import at.mocode.infrastructure.messaging.client.EventPublisher
import at.mocode.core.domain.model.PferdeGeschlechtE
import io.mockk.mockk
import kotlinx.datetime.LocalDate
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestPropertySource
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Integration tests for the Horses Service.
*
* These tests verify the complete functionality including:
* - Repository operations
* - Database persistence
* - Domain model validation
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@TestPropertySource(properties = [
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.kafka.bootstrap-servers=localhost:9092"
])
@ContextConfiguration(classes = [HorseServiceIntegrationTest.TestConfig::class])
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class HorseServiceIntegrationTest {
@Autowired
private lateinit var horseRepository: HorseRepository
@Configuration
class TestConfig {
@Bean
fun eventPublisher(): EventPublisher = mockk(relaxed = true)
}
@BeforeEach
fun setUp() = runBlocking {
// Clean up database before each test
println("[DEBUG_LOG] Setting up horse test - cleaning database")
}
@Test
fun `should create horse successfully`() = runBlocking {
println("[DEBUG_LOG] Testing horse creation")
// Given
val horse = DomPferd(
pferdeName = "Thunder",
geschlecht = PferdeGeschlechtE.WALLACH,
geburtsdatum = LocalDate(2020, 5, 15),
rasse = "Warmblut",
farbe = "Braun",
lebensnummer = "AT123456789",
chipNummer = "123456789012345",
stockmass = 165,
istAktiv = true
)
// When
val savedHorse = horseRepository.save(horse)
// Then
assertNotNull(savedHorse)
assertEquals("Thunder", savedHorse.pferdeName)
assertEquals(PferdeGeschlechtE.WALLACH, savedHorse.geschlecht)
assertEquals("AT123456789", savedHorse.lebensnummer)
assertEquals("123456789012345", savedHorse.chipNummer)
assertEquals("Warmblut", savedHorse.rasse)
assertTrue(savedHorse.istAktiv)
println("[DEBUG_LOG] Horse created successfully with ID: ${savedHorse.pferdId}")
}
@Test
fun `should find horse by lebensnummer`() = runBlocking {
println("[DEBUG_LOG] Testing find horse by lebensnummer")
// Given
val horse = DomPferd(
pferdeName = "Lightning",
geschlecht = PferdeGeschlechtE.STUTE,
geburtsdatum = LocalDate(2019, 3, 10),
rasse = "Vollblut",
farbe = "Schimmel",
lebensnummer = "AT987654321",
chipNummer = "987654321098765",
stockmass = 160,
istAktiv = true
)
horseRepository.save(horse)
// When
val foundHorse = horseRepository.findByLebensnummer("AT987654321")
// Then
assertNotNull(foundHorse)
assertEquals("Lightning", foundHorse.pferdeName)
assertEquals("AT987654321", foundHorse.lebensnummer)
assertEquals(PferdeGeschlechtE.STUTE, foundHorse.geschlecht)
assertEquals("Vollblut", foundHorse.rasse)
println("[DEBUG_LOG] Horse found by lebensnummer: ${foundHorse.pferdId}")
}
@Test
fun `should find horse by chip number`() = runBlocking {
println("[DEBUG_LOG] Testing find horse by chip number")
// Given
val horse = DomPferd(
pferdeName = "Storm",
geschlecht = PferdeGeschlechtE.HENGST,
geburtsdatum = LocalDate(2021, 8, 20),
rasse = "Haflinger",
farbe = "Fuchs",
lebensnummer = "AT555666777",
chipNummer = "555666777888999",
stockmass = 150,
istAktiv = true
)
horseRepository.save(horse)
// When
val foundHorse = horseRepository.findByChipNummer("555666777888999")
// Then
assertNotNull(foundHorse)
assertEquals("Storm", foundHorse.pferdeName)
assertEquals("555666777888999", foundHorse.chipNummer)
assertEquals(PferdeGeschlechtE.HENGST, foundHorse.geschlecht)
assertEquals("Haflinger", foundHorse.rasse)
println("[DEBUG_LOG] Horse found by chip number: ${foundHorse.pferdId}")
}
@Test
fun `should find horses by gender`() = runBlocking {
println("[DEBUG_LOG] Testing find horses by gender")
// Given
val stallion = DomPferd(
pferdeName = "Stallion Horse",
geschlecht = PferdeGeschlechtE.HENGST,
geburtsdatum = LocalDate(2018, 4, 12),
rasse = "Warmblut",
farbe = "Braun",
lebensnummer = "AT111222333",
chipNummer = "111222333444555",
stockmass = 170,
istAktiv = true
)
val mare = DomPferd(
pferdeName = "Mare Horse",
geschlecht = PferdeGeschlechtE.STUTE,
geburtsdatum = LocalDate(2017, 6, 8),
rasse = "Vollblut",
farbe = "Rappe",
lebensnummer = "AT444555666",
chipNummer = "444555666777888",
stockmass = 165,
istAktiv = true
)
horseRepository.save(stallion)
horseRepository.save(mare)
// When
val stallions = horseRepository.findByGeschlecht(PferdeGeschlechtE.HENGST, true, 10)
// Then
assertTrue(stallions.isNotEmpty(), "Should find at least one stallion")
assertTrue(stallions.any { it.pferdeName == "Stallion Horse" }, "Should contain the stallion horse")
assertTrue(stallions.all { it.geschlecht == PferdeGeschlechtE.HENGST }, "All returned horses should be stallions")
println("[DEBUG_LOG] Found ${stallions.size} stallions")
}
@Test
fun `should find horses by breed`() = runBlocking {
println("[DEBUG_LOG] Testing find horses by breed")
// Given
val warmblutHorse = DomPferd(
pferdeName = "Warmblut Horse",
geschlecht = PferdeGeschlechtE.WALLACH,
geburtsdatum = LocalDate(2019, 9, 15),
rasse = "Warmblut",
farbe = "Braun",
lebensnummer = "AT333444555",
chipNummer = "333444555666777",
stockmass = 168,
istAktiv = true
)
horseRepository.save(warmblutHorse)
// When
val warmblutHorses = horseRepository.findByRasse("Warmblut", true, 10)
// Then
assertTrue(warmblutHorses.isNotEmpty(), "Should find at least one Warmblut horse")
assertTrue(warmblutHorses.any { it.pferdeName == "Warmblut Horse" }, "Should contain the Warmblut horse")
assertTrue(warmblutHorses.all { it.rasse == "Warmblut" }, "All returned horses should be Warmblut")
println("[DEBUG_LOG] Found ${warmblutHorses.size} Warmblut horses")
}
@Test
fun `should find OEPS registered horses`() = runBlocking {
println("[DEBUG_LOG] Testing find OEPS registered horses")
// Given
val oepsHorse = DomPferd(
pferdeName = "OEPS Horse",
geschlecht = PferdeGeschlechtE.WALLACH,
geburtsdatum = LocalDate(2018, 7, 22),
rasse = "Warmblut",
farbe = "Braun",
lebensnummer = "AT777888999",
chipNummer = "777888999000111",
oepsNummer = "OEPS123456",
stockmass = 170,
istAktiv = true
)
val nonOepsHorse = DomPferd(
pferdeName = "Non-OEPS Horse",
geschlecht = PferdeGeschlechtE.STUTE,
geburtsdatum = LocalDate(2017, 11, 5),
rasse = "Vollblut",
farbe = "Rappe",
lebensnummer = "AT000111222",
chipNummer = "000111222333444",
stockmass = 165,
istAktiv = true
)
horseRepository.save(oepsHorse)
horseRepository.save(nonOepsHorse)
// When
val oepsHorses = horseRepository.findOepsRegistered(true)
// Then
assertTrue(oepsHorses.isNotEmpty(), "Should find at least one OEPS registered horse")
assertTrue(oepsHorses.any { it.pferdeName == "OEPS Horse" }, "Should contain the OEPS registered horse")
assertTrue(oepsHorses.all { !it.oepsNummer.isNullOrBlank() }, "All returned horses should have OEPS numbers")
println("[DEBUG_LOG] Found ${oepsHorses.size} OEPS registered horses")
}
@Test
fun `should find FEI registered horses`() = runBlocking {
println("[DEBUG_LOG] Testing find FEI registered horses")
// Given
val feiHorse = DomPferd(
pferdeName = "FEI Horse",
geschlecht = PferdeGeschlechtE.HENGST,
geburtsdatum = LocalDate(2016, 2, 14),
rasse = "Warmblut",
farbe = "Schimmel",
lebensnummer = "AT999000111",
chipNummer = "999000111222333",
feiNummer = "FEI789012",
stockmass = 175,
istAktiv = true
)
horseRepository.save(feiHorse)
// When
val feiHorses = horseRepository.findFeiRegistered(true)
// Then
assertTrue(feiHorses.isNotEmpty(), "Should find at least one FEI registered horse")
assertTrue(feiHorses.any { it.pferdeName == "FEI Horse" }, "Should contain the FEI registered horse")
assertTrue(feiHorses.all { !it.feiNummer.isNullOrBlank() }, "All returned horses should have FEI numbers")
println("[DEBUG_LOG] Found ${feiHorses.size} FEI registered horses")
}
@Test
fun `should validate duplicate lebensnummer`() = runBlocking {
println("[DEBUG_LOG] Testing duplicate lebensnummer validation")
// Given
val horse = DomPferd(
pferdeName = "First Horse",
geschlecht = PferdeGeschlechtE.WALLACH,
geburtsdatum = LocalDate(2019, 1, 1),
rasse = "Warmblut",
farbe = "Braun",
lebensnummer = "AT123123123",
chipNummer = "123123123456789",
stockmass = 165,
istAktiv = true
)
horseRepository.save(horse)
// When
val exists = horseRepository.existsByLebensnummer("AT123123123")
// Then
assertTrue(exists, "Should detect existing lebensnummer")
println("[DEBUG_LOG] Duplicate lebensnummer validation passed")
}
@Test
fun `should validate duplicate chip number`() = runBlocking {
println("[DEBUG_LOG] Testing duplicate chip number validation")
// Given
val horse = DomPferd(
pferdeName = "Chip Test Horse",
geschlecht = PferdeGeschlechtE.STUTE,
geburtsdatum = LocalDate(2020, 12, 25),
rasse = "Haflinger",
farbe = "Fuchs",
lebensnummer = "AT456456456",
chipNummer = "456456456789012",
stockmass = 148,
istAktiv = true
)
horseRepository.save(horse)
// When
val exists = horseRepository.existsByChipNummer("456456456789012")
// Then
assertTrue(exists, "Should detect existing chip number")
println("[DEBUG_LOG] Duplicate chip number validation passed")
}
}
@@ -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")
}
}
@@ -0,0 +1,10 @@
<configuration>
<appender name="CONSOLE" 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="CONSOLE" />
</root>
</configuration>