refactor(core): Stabilize and Refactor Shared Kernel
This commit introduces a comprehensive refactoring and stabilization of the core module, establishing a robust and well-tested foundation (Shared Kernel) for all other services. The module has been thoroughly analyzed, cleaned up, and equipped with a professional-grade test suite. Architectural Refinements: - Slimmed down `core-domain` to be a true, minimal Shared Kernel by removing all domain-specific enums (`PferdeGeschlechtE`, `SparteE`, etc.). This enforces loose coupling between feature modules. - The only remaining enum is `DatenQuelleE`, which is a cross-cutting concern. Code Refactoring & Improvements: - Refactored the configuration loading by introducing a `ConfigLoader` class. This decouples the `AppConfig` data classes from the loading mechanism, significantly improving the testability of components that rely on configuration. - Unified the previously duplicated `ValidationResult` and `ValidationError` classes into a single, serializable source of truth, ensuring consistent error reporting across all APIs. Testing Enhancements: - Introduced a comprehensive test suite for the core module, bringing it to a production-ready quality standard. - Implemented the "gold standard" for database testing by replacing the previous H2 approach with **Testcontainers**. The `DatabaseFactory` is now tested against a real, ephemeral PostgreSQL container, guaranteeing 100% production parity. - Added robust unit and integration tests for critical components, including the new `ConfigLoader`, all custom `Serializers`, and the `ApiResponse` logic. - Fixed all compilation and runtime errors in the test suite, resulting in a successful `./gradlew clean build`.
This commit is contained in:
@@ -3,105 +3,13 @@ package at.mocode.core.domain.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Defines the source of a data record.
|
||||
* Defines the source of a data record. This is a cross-cutting concern
|
||||
* and therefore part of the Shared Kernel.
|
||||
*/
|
||||
@Serializable
|
||||
enum class DatenQuelleE {
|
||||
MANUELL, // Manually entered
|
||||
IMPORT_ZNS, // Imported from OEPS ZNS data
|
||||
SYSTEM_GENERATED // Generated by the system itself
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the gender of a horse.
|
||||
*/
|
||||
@Serializable
|
||||
enum class PferdeGeschlechtE {
|
||||
HENGST, // Male, not castrated
|
||||
STUTE, // Female
|
||||
WALLACH // Male, castrated
|
||||
}
|
||||
|
||||
/**
|
||||
* Person gender enumeration
|
||||
*/
|
||||
@Serializable
|
||||
enum class GeschlechtE { M, W, D, UNBEKANNT }
|
||||
|
||||
/**
|
||||
* Defines the different equestrian disciplines (Sparten).
|
||||
* This enum is a central part of the Ubiquitous Language.
|
||||
*/
|
||||
@Serializable
|
||||
enum class SparteE {
|
||||
DRESSUR, // Dressurreiten
|
||||
SPRINGEN, // Springreiten
|
||||
VIELSEITIGKEIT, // Vielseitigkeitsreiten
|
||||
FAHREN, // Fahrsport
|
||||
VOLTIGIEREN, // Voltigieren
|
||||
WESTERN, // Westernreiten
|
||||
DISTANZ, // Distanzreiten
|
||||
PARA_DRESSUR, // Para-Dressur
|
||||
ISLAND; // Islandpferde
|
||||
}
|
||||
|
||||
/**
|
||||
* Venue/place type enumeration
|
||||
*/
|
||||
@Serializable
|
||||
enum class PlatzTypE { AUSTRAGUNG, VORBEREITUNG, LONGIEREN, SONSTIGES }
|
||||
|
||||
/**
|
||||
* User role enumeration for member management
|
||||
*/
|
||||
@Serializable
|
||||
enum class RolleE {
|
||||
ADMIN, // System administrator
|
||||
VEREINS_ADMIN, // Club administrator
|
||||
FUNKTIONAER, // Official/functionary
|
||||
REITER, // Rider
|
||||
TRAINER, // Trainer
|
||||
RICHTER, // Judge
|
||||
TIERARZT, // Veterinarian
|
||||
ZUSCHAUER, // Spectator
|
||||
GAST // Guest
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission enumeration for access control
|
||||
*/
|
||||
@Serializable
|
||||
enum class BerechtigungE {
|
||||
// Person management
|
||||
PERSON_READ,
|
||||
PERSON_CREATE,
|
||||
PERSON_UPDATE,
|
||||
PERSON_DELETE,
|
||||
|
||||
// Club management
|
||||
VEREIN_READ,
|
||||
VEREIN_CREATE,
|
||||
VEREIN_UPDATE,
|
||||
VEREIN_DELETE,
|
||||
|
||||
// Event management
|
||||
VERANSTALTUNG_READ,
|
||||
VERANSTALTUNG_CREATE,
|
||||
VERANSTALTUNG_UPDATE,
|
||||
VERANSTALTUNG_DELETE,
|
||||
|
||||
// Horse management
|
||||
PFERD_READ,
|
||||
PFERD_CREATE,
|
||||
PFERD_UPDATE,
|
||||
PFERD_DELETE,
|
||||
|
||||
// Master data management
|
||||
STAMMDATEN_READ,
|
||||
STAMMDATEN_UPDATE,
|
||||
|
||||
// System administration
|
||||
SYSTEM_ADMIN,
|
||||
BENUTZER_VERWALTEN,
|
||||
ROLLEN_VERWALTEN
|
||||
MANUELL,
|
||||
IMPORT_ZNS,
|
||||
SYSTEM_GENERATED,
|
||||
IMPORT_API
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package at.mocode.core.domain
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ApiResponseTest {
|
||||
|
||||
@Test
|
||||
fun `ApiResponse success should create a successful response with data`() {
|
||||
// Arrange
|
||||
val testData = "This is a test"
|
||||
|
||||
// Act
|
||||
val response = ApiResponse.success(testData)
|
||||
|
||||
// Assert
|
||||
assertTrue(response.success, "Response should be successful")
|
||||
assertEquals(testData, response.data, "Response data should match the input data")
|
||||
assertTrue(response.errors.isEmpty(), "Errors list should be empty for a successful response")
|
||||
assertNotNull(response.timestamp, "Timestamp should be generated")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ApiResponse error with single message should create a failed response with one error`() {
|
||||
// Arrange
|
||||
val errorCode = "NOT_FOUND"
|
||||
val errorMessage = "The requested resource was not found."
|
||||
val errorField = "resourceId"
|
||||
|
||||
// Act
|
||||
val response = ApiResponse.error<Unit>(errorCode, errorMessage, errorField)
|
||||
|
||||
// Assert
|
||||
assertFalse(response.success, "Response should not be successful")
|
||||
assertNull(response.data, "Data should be null for a failed response")
|
||||
assertEquals(1, response.errors.size, "Should contain exactly one error")
|
||||
|
||||
val error = response.errors.first()
|
||||
assertEquals(errorCode, error.code, "Error code should match")
|
||||
assertEquals(errorMessage, error.message, "Error message should match")
|
||||
assertEquals(errorField, error.field, "Error field should match")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ApiResponse error with list should create a failed response with multiple errors`() {
|
||||
// Arrange
|
||||
val errors = listOf(
|
||||
ErrorDto("INVALID_INPUT", "Username cannot be empty.", "username"),
|
||||
ErrorDto("INVALID_INPUT", "Password is too short.", "password")
|
||||
)
|
||||
|
||||
// Act
|
||||
val response = ApiResponse.error<Unit>(errors)
|
||||
|
||||
// Assert
|
||||
assertFalse(response.success, "Response should not be successful")
|
||||
assertNull(response.data, "Data should be null for a failed response")
|
||||
assertEquals(2, response.errors.size, "Should contain two errors")
|
||||
assertEquals(errors, response.errors, "The error list should match the input list")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package at.mocode.core.domain
|
||||
|
||||
import at.mocode.core.domain.event.BaseDomainEvent
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DomainEventTest {
|
||||
|
||||
/**
|
||||
* Eine konkrete Implementierung eines Domänen-Events zu Testzwecken.
|
||||
* Repräsentiert das Ereignis, dass eine Test-Entität erstellt wurde.
|
||||
*
|
||||
* @param aggregateId Die ID der Entität, auf die sich das Event bezieht.
|
||||
* @param version Die Versionsnummer des Events für dieses Aggregat.
|
||||
* @param testPayload Ein zusätzliches Datenfeld, das für den Test relevant ist.
|
||||
*/
|
||||
@Serializable
|
||||
data class TestEvent(
|
||||
@Transient
|
||||
override val aggregateId: Uuid = uuid4(),
|
||||
@Transient
|
||||
override val version: Long = 1L,
|
||||
val testPayload: String = "Test"
|
||||
) : BaseDomainEvent(
|
||||
aggregateId = aggregateId,
|
||||
eventType = "TestEventOccurred", // Ein klar definierter Event-Typ
|
||||
version = version
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `BaseDomainEvent should auto-generate eventId and timestamp upon creation`() {
|
||||
// Arrange
|
||||
val aggregateId = uuid4()
|
||||
val version = 1L
|
||||
|
||||
// Act
|
||||
val event = TestEvent(aggregateId, version)
|
||||
|
||||
// Assert
|
||||
assertNotNull(event.eventId, "eventId should be automatically generated and not null")
|
||||
assertNotNull(event.timestamp, "timestamp should be automatically generated and not null")
|
||||
assertEquals(aggregateId, event.aggregateId, "aggregateId should be set correctly")
|
||||
assertEquals(version, event.version, "version should be set correctly")
|
||||
assertEquals("TestEventOccurred", event.eventType, "eventType should be set correctly")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package at.mocode.core.domain
|
||||
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
||||
import at.mocode.core.domain.serialization.KotlinLocalDateTimeSerializer
|
||||
import at.mocode.core.domain.serialization.KotlinLocalTimeSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
class SerializersTest {
|
||||
|
||||
private val json = Json // Standard-Json-Konfiguration für die Tests
|
||||
|
||||
// Hilfsklasse, um die Serializer im Kontext von kotlinx.serialization zu testen
|
||||
@Serializable
|
||||
data class TestContainer(
|
||||
@Serializable(with = UuidSerializer::class) val uuid: Uuid,
|
||||
@Serializable(with = KotlinInstantSerializer::class) val instant: Instant,
|
||||
@Serializable(with = KotlinLocalDateSerializer::class) val localDate: LocalDate,
|
||||
@Serializable(with = KotlinLocalDateTimeSerializer::class) val localDateTime: LocalDateTime,
|
||||
@Serializable(with = KotlinLocalTimeSerializer::class) val localTime: LocalTime
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `all custom serializers should correctly serialize and deserialize`() {
|
||||
// Arrange
|
||||
val originalObject = TestContainer(
|
||||
uuid = uuid4(),
|
||||
instant = Clock.System.now(),
|
||||
localDate = LocalDate(2025, 8, 5),
|
||||
localDateTime = LocalDateTime(2025, 8, 5, 12, 30, 0),
|
||||
localTime = LocalTime(12, 30, 0)
|
||||
)
|
||||
|
||||
// Act: Serialize
|
||||
val jsonString = json.encodeToString(TestContainer.serializer(), originalObject)
|
||||
|
||||
// Assert: Serialization
|
||||
// Wir prüfen, ob die serialisierten Werte einfache Strings sind, wie erwartet.
|
||||
assertTrue(
|
||||
jsonString.contains("\"uuid\":\"${originalObject.uuid}\""),
|
||||
"Serialized JSON should contain the UUID as a string"
|
||||
)
|
||||
assertTrue(
|
||||
jsonString.contains("\"instant\":\"${originalObject.instant}\""),
|
||||
"Serialized JSON should contain the Instant as a string"
|
||||
)
|
||||
assertTrue(
|
||||
jsonString.contains("\"localDate\":\"2025-08-05\""),
|
||||
"Serialized JSON should contain the LocalDate as a string"
|
||||
)
|
||||
assertTrue(
|
||||
jsonString.contains("\"localDateTime\":\"2025-08-05T12:30\""),
|
||||
"Serialized JSON should contain the LocalDateTime as a string"
|
||||
)
|
||||
assertTrue(
|
||||
jsonString.contains("\"localTime\":\"12:30\""),
|
||||
"Serialized JSON should contain the LocalTime as a string"
|
||||
)
|
||||
|
||||
// Act: Deserialize
|
||||
val deserializedObject = json.decodeFromString(TestContainer.serializer(), jsonString)
|
||||
|
||||
// Assert: Deserialization
|
||||
assertEquals(originalObject, deserializedObject, "Deserialized object should be equal to the original object")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user