fixing(gradle)

This commit is contained in:
2025-08-24 21:31:31 +02:00
parent 8d01fa0e9a
commit 89ef9698af
77 changed files with 2060 additions and 1656 deletions
+31 -13
View File
@@ -1,17 +1,16 @@
// Dieses Modul definiert die Kern-Domänenobjekte des Shared kernels.
// Es enthält keine Implementierungsdetails, nur reine Datenklassen und Enums.
// Core domain objects of the Shared kernel
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
}
kotlin {
// Target platforms
jvm {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
}
js(IR) {
browser()
}
@@ -19,33 +18,52 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
// Kern-Abhängigkeiten für das Domänen-Modul (common for all platforms)
// Core dependencies (that aren't included in platform-dependencies)
api(libs.uuid)
// Serialization and date-time for commonMain
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)
}
}
val jvmMain by getting {
dependencies {
// Stellt sicher, dass dieses Modul Zugriff auf die im zentralen Katalog
// definierten Bibliotheken hat (JVM-specific)
api(projects.platform.platformDependencies)
}
}
val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
}
}
val jsMain by getting {
dependencies {
api(libs.kotlinx.coroutines.core)
}
}
val jsTest by getting {
dependencies {
implementation(libs.kotlin.test)
}
}
val jvmMain by getting {
dependencies {
// Fachliches Domain-Modul: keine technischen Abhängigkeiten hier hinterlegen.
// Falls in Zukunft JVM-spezifische, fachlich neutrale Ergänzungen nötig sind,
// bitte bewusst und minimal hinzufügen.
}
}
val jvmTest by getting {
dependencies {
// Stellt die Test-Bibliotheken bereit (JVM-specific)
// implementation(kotlin("test-junit5"))
implementation(libs.junit.jupiter.api)
implementation(libs.mockk)
implementation(projects.platform.platformTesting)
implementation(libs.bundles.testing.jvm)
}
}
}
}
tasks.named<Test>("jvmTest") {
useJUnitPlatform()
}
@@ -2,20 +2,17 @@ package at.mocode.core.domain.event
import at.mocode.core.domain.model.*
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import kotlinx.serialization.Serializable
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class)
/**
* Basis-Interface für alle Domänen-Events im System.
* Ein Domänen-Event repräsentiert etwas fachlich Bedeutsames, das passiert ist.
* Basis-Interface für alle Domain-Events im System.
* Ein Domain-Event beschreibt ein fachlich relevantes Ereignis, das stattgefunden hat.
*/
@OptIn(ExperimentalTime::class)
interface DomainEvent {
val eventId: EventId
val aggregateId: AggregateId
@@ -27,7 +24,7 @@ interface DomainEvent {
}
/**
* Abstrakte Basisklasse für Domänen-Events, um Boilerplate-Code zu reduzieren.
* Abstrakte Basisklasse für Domain-Events, um Boilerplate zu reduzieren.
*/
@Serializable
@OptIn(ExperimentalTime::class)
@@ -37,13 +34,36 @@ abstract class BaseDomainEvent(
override val version: EventVersion,
override val eventId: EventId = EventId(uuid4()),
@Serializable(with = KotlinInstantSerializer::class)
override val timestamp: Instant = Clock.System.now(),
override val timestamp: Instant,
override val correlationId: CorrelationId? = null,
override val causationId: CausationId? = null
) : DomainEvent
) : DomainEvent {
constructor(
aggregateId: AggregateId,
eventType: EventType,
version: EventVersion,
eventId: EventId = EventId(uuid4()),
correlationId: CorrelationId? = null,
causationId: CausationId? = null
) : this(
aggregateId = aggregateId,
eventType = eventType,
version = version,
eventId = eventId,
timestamp = createTimestamp(),
correlationId = correlationId,
causationId = causationId
)
companion object {
@OptIn(ExperimentalTime::class)
private fun createTimestamp(): Instant = Clock.System.now()
}
}
/**
* Interface für einen Publisher, der Domänen-Events veröffentlichen kann.
* Schnittstelle für einen Publisher, der Domain-Events veröffentlichen kann.
*/
interface DomainEventPublisher {
suspend fun publish(event: DomainEvent)
@@ -51,9 +71,14 @@ interface DomainEventPublisher {
}
/**
* Interface für einen Handler, der auf bestimmte Domänen-Events reagieren kann.
* Schnittstelle für einen Handler, der auf bestimmte Domain-Events reagieren kann.
*/
interface DomainEventHandler<T : DomainEvent> {
suspend fun handle(event: T)
fun canHandle(eventType: String): Boolean
fun canHandle(eventType: EventType): Boolean
/**
* Rückwärtskompatible Methode für String-basierte Prüfung des Event-Typs.
*/
fun canHandle(eventType: String): Boolean = canHandle(EventType(eventType))
}
@@ -1,20 +1,18 @@
package at.mocode.core.domain.model
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.time.ExperimentalTime
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
/**
* A marker interface for all Data Transfer Objects.
* Marker-Interface für alle Data-Transfer-Objekte (DTO).
*/
interface BaseDto
/**
* Base DTO for domain entities that have unique ID and audit timestamps.
* Basis-DTO für Domänen-Entitäten mit eindeutiger ID und Audit-Zeitstempeln.
*/
@Serializable
@OptIn(ExperimentalTime::class)
@@ -29,17 +27,17 @@ abstract class EntityDto : BaseDto {
}
/**
* A structured representation of a single error.
* Strukturierte Darstellung eines einzelnen Fehlers (Code, Nachricht, optionales Feld).
*/
@Serializable
data class ErrorDto(
val code: String,
val code: ErrorCode,
val message: String,
val field: String? = null
) : BaseDto
/**
* A standardized and consistent wrapper for all API responses.
* Standardisierte Hülle für API-Antworten mit einheitlicher Struktur.
*/
@Serializable
@OptIn(ExperimentalTime::class)
@@ -48,12 +46,26 @@ data class ApiResponse<T>(
val success: Boolean,
val errors: List<ErrorDto> = emptyList(),
@Serializable(with = KotlinInstantSerializer::class)
val timestamp: Instant = Clock.System.now()
val timestamp: Instant
) {
companion object {
@OptIn(ExperimentalTime::class)
fun <T> success(data: T): ApiResponse<T> {
return ApiResponse(data = data, success = true)
return ApiResponse(data = data, success = true, timestamp = Clock.System.now())
}
@OptIn(ExperimentalTime::class)
fun <T> error(
code: ErrorCode,
message: String,
field: String? = null
): ApiResponse<T> {
return ApiResponse(
data = null,
success = false,
errors = listOf(ErrorDto(code = code, message = message, field = field)),
timestamp = Clock.System.now()
)
}
@OptIn(ExperimentalTime::class)
@@ -62,30 +74,52 @@ data class ApiResponse<T>(
message: String,
field: String? = null
): ApiResponse<T> {
return ApiResponse(
data = null,
success = false,
errors = listOf(ErrorDto(code = code, message = message, field = field))
)
return error(ErrorCode(code), message, field)
}
@OptIn(ExperimentalTime::class)
fun <T> error(errors: List<ErrorDto>): ApiResponse<T> {
return ApiResponse(data = null, success = false, errors = errors)
return ApiResponse(data = null, success = false, errors = errors, timestamp = Clock.System.now())
}
}
}
/**
* A standardized wrapper for paginated API responses.
* Standardisierte Hülle für paginierte API-Antworten.
*/
@Serializable
data class PagedResponse<T>(
val content: List<T>,
val page: Int,
val size: Int,
val page: PageNumber,
val size: PageSize,
val totalElements: Long,
val totalPages: Int,
val hasNext: Boolean,
val hasPrevious: Boolean
)
) {
companion object {
/**
* Erzeugt eine PagedResponse mit Rückwärtskompatibilität für einfache Int-Werte.
* Nützlich, wenn Aufrufer noch keine PageNumber/PageSize verwenden.
*/
fun <T> create(
content: List<T>,
page: Int,
size: Int,
totalElements: Long,
totalPages: Int,
hasNext: Boolean,
hasPrevious: Boolean
): PagedResponse<T> {
return PagedResponse(
content = content,
page = PageNumber(page),
size = PageSize(size),
totalElements = totalElements,
totalPages = totalPages,
hasNext = hasNext,
hasPrevious = hasPrevious
)
}
}
}
@@ -0,0 +1,79 @@
package at.mocode.core.domain.model
import kotlinx.serialization.Serializable
/**
* Gemeinsame Enums, die domänenweit verwendet werden.
* Teil des Shared Kernel zur Sicherung einer konsistenten Fachsprache.
*/
/**
* Quelle eines Datensatzes. Querschnittsthema und daher Teil des Shared Kernel.
*/
@Serializable
enum class DatenQuelleE {
MANUELL,
IMPORT_ZNS,
SYSTEM_GENERATED,
IMPORT_API
}
/**
* Allgemeiner Status von Entitäten in der Domäne.
*/
@Serializable
enum class StatusE {
AKTIV,
INAKTIV,
ENTWURF,
ARCHIVIERT,
GELOESCHT
}
/**
* Prioritätsstufen für unterschiedliche Domänen-Objekte.
*/
@Serializable
enum class PrioritaetE {
NIEDRIG,
NORMAL,
HOCH,
KRITISCH
}
/**
* Häufige Benutzerrollen im System.
*/
@Serializable
enum class BenutzerRolleE {
ADMIN,
BENUTZER,
MODERATOR,
GAST,
SYSTEM
}
/**
* Verifikationsstatus für Datensätze.
*/
@Serializable
enum class VerifikationsStatusE {
NICHT_VERIFIZIERT,
IN_PRUEFUNG,
VERIFIZIERT,
ABGELEHNT,
KORREKTUR_ERFORDERLICH
}
/**
* Processing states for workflows and tasks.
*/
@Serializable
enum class BearbeitungsStatusE {
OFFEN,
IN_BEARBEITUNG,
WARTEND,
ABGESCHLOSSEN,
ABGEBROCHEN,
FEHLER
}
@@ -0,0 +1,45 @@
package at.mocode.core.domain.model
import kotlinx.serialization.Serializable
/**
* Repräsentiert einen Validierungsfehler mit Feldname, Nachricht und Fehlercode.
* Wird von Validierungs-Hilfsfunktionen im gesamten System verwendet.
*/
@Serializable
data class ValidationError(
val field: String,
val message: String,
val code: String
) : BaseDto {
companion object {
/**
* Erzeugt einen Validierungsfehler für Pflichtfeld-Prüfungen.
*/
fun required(field: String): ValidationError {
return ValidationError(field, "$field ist erforderlich", "REQUIRED")
}
/**
* Erzeugt einen Validierungsfehler für ungültiges Format.
*/
fun invalidFormat(field: String, message: String = "Ungültiges Format"): ValidationError {
return ValidationError(field, message, "INVALID_FORMAT")
}
/**
* Erzeugt einen Validierungsfehler für Längenprüfungen.
*/
fun invalidLength(field: String, message: String): ValidationError {
return ValidationError(field, message, "INVALID_LENGTH")
}
/**
* Erzeugt einen Validierungsfehler für Bereichsprüfungen.
*/
fun invalidRange(field: String, message: String): ValidationError {
return ValidationError(field, message, "INVALID_RANGE")
}
}
}
@@ -2,23 +2,23 @@ package at.mocode.core.domain.model
import at.mocode.core.domain.serialization.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
/**
* Value classes for strongly typed IDs and domain values.
* These provide compile-time type safety without runtime overhead.
* Value-Classes für stark typisierte IDs und Fachwerte.
* Bieten Typsicherheit zur Compile-Zeit ohne Laufzeit-Overhead.
*/
// === ID Value Classes ===
/**
* A strongly typed wrapper for entity IDs.
* Stark typisierte Hülle für Entitäts-IDs.
*/
@Serializable
@JvmInline
value class EntityId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
companion object
}
/**
@@ -27,7 +27,7 @@ value class EntityId(@Serializable(with = UuidSerializer::class) val value: Uuid
@Serializable
@JvmInline
value class EventId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
companion object
}
/**
@@ -36,7 +36,7 @@ value class EventId(@Serializable(with = UuidSerializer::class) val value: Uuid)
@Serializable
@JvmInline
value class AggregateId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
companion object
}
/**
@@ -45,7 +45,7 @@ value class AggregateId(@Serializable(with = UuidSerializer::class) val value: U
@Serializable
@JvmInline
value class CorrelationId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
companion object
}
/**
@@ -54,7 +54,7 @@ value class CorrelationId(@Serializable(with = UuidSerializer::class) val value:
@Serializable
@JvmInline
value class CausationId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
companion object
}
// === Domain Value Classes ===
@@ -2,8 +2,6 @@ package at.mocode.core.domain.serialization
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
import kotlin.time.Instant // KORRIGIERT: Finaler Wechsel zu kotlin.time
import kotlin.time.ExperimentalTime
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
@@ -13,34 +11,86 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
object UuidSerializer : KSerializer<Uuid> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uuid) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Uuid = uuidFrom(decoder.decodeString())
}
/**
* Serializer für kotlin.time. Instant Objekte.
* Konvertiert Instant zu/von ISO-8601 String-Repräsentation.
*/
@OptIn(ExperimentalTime::class)
object KotlinInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.parse(decoder.decodeString())
}
}
object KotlinLocalDateSerializer : KSerializer<LocalDate> {
/**
* Serializer für UUID Objekte.
* Konvertiert UUID zu/von String-Repräsentation.
*/
object UuidSerializer : KSerializer<Uuid> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uuid) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Uuid {
return uuidFrom(decoder.decodeString())
}
}
/**
* Serializer für kotlinx.datetime.LocalDate Objekte.
* Konvertiert LocalDate zu/von ISO-8601 String-Repräsentation.
*/
object LocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDate) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): LocalDate = LocalDate.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDate {
return LocalDate.parse(decoder.decodeString())
}
}
object KotlinLocalDateTimeSerializer : KSerializer<LocalDateTime> {
/**
* Serializer für kotlinx.datetime. LocalDateTime Objekte.
* Konvertiert LocalDateTime zu/von ISO-8601 String-Repräsentation.
*/
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDateTime) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): LocalDateTime = LocalDateTime.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString())
}
}
object KotlinLocalTimeSerializer : KSerializer<LocalTime> {
/**
* Serializer für kotlinx.datetime.LocalTime Objekte.
* Konvertiert LocalTime zu/von ISO-8601 String-Repräsentation.
*/
object LocalTimeSerializer : KSerializer<LocalTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalTime) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): LocalTime = LocalTime.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: LocalTime) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalTime {
return LocalTime.parse(decoder.decodeString())
}
}
@@ -0,0 +1,53 @@
package at.mocode.core.domain
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.ErrorCode
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@OptIn(kotlin.time.ExperimentalTime::class)
class ApiResponseTest {
@Test
fun `success factory sets flags and timestamp`() {
val res = ApiResponse.success(data = 42)
assertTrue(res.success)
assertEquals(42, res.data)
assertTrue(res.errors.isEmpty())
assertNotNull(res.timestamp)
}
@Test
fun `error factory with code object`() {
val res = ApiResponse.error<Int>(ErrorCode("INVALID_INPUT"), "Fehlerhafte Eingabe", field = "name")
assertFalse(res.success)
assertNull(res.data)
assertEquals(1, res.errors.size)
assertEquals("INVALID_INPUT", res.errors.first().code.value)
assertEquals("Fehlerhafte Eingabe", res.errors.first().message)
assertEquals("name", res.errors.first().field)
assertNotNull(res.timestamp)
}
@Test
fun `error factory with code string`() {
val res = ApiResponse.error<Int>("NOT_FOUND", "Nicht gefunden")
assertFalse(res.success)
assertNull(res.data)
assertEquals(1, res.errors.size)
assertEquals("NOT_FOUND", res.errors.first().code.value)
}
@Test
fun `error factory with list`() {
val res = ApiResponse.error<Int>(listOf())
assertFalse(res.success)
assertNull(res.data)
assertTrue(res.errors.isEmpty())
assertNotNull(res.timestamp)
}
}
@@ -0,0 +1,63 @@
package at.mocode.core.domain
import at.mocode.core.domain.event.BaseDomainEvent
import at.mocode.core.domain.model.*
import com.benasher44.uuid.uuid4
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@OptIn(kotlin.time.ExperimentalTime::class)
class BaseDomainEventTest {
@kotlinx.serialization.Serializable
data class TestEvent(
val name: String,
// Delegiert an BaseDomainEvent
private val base: BaseDomainEvent
) : BaseDomainEvent(
aggregateId = base.aggregateId,
eventType = base.eventType,
version = base.version,
eventId = base.eventId,
timestamp = base.timestamp,
correlationId = base.correlationId,
causationId = base.causationId
)
@Test
fun `secondary constructor generates id and timestamp`() {
val aggId = AggregateId(uuid4())
val ev = object : BaseDomainEvent(
aggregateId = aggId,
eventType = EventType("TestEvent"),
version = EventVersion(1)
) {}
assertNotNull(ev.eventId)
assertNotNull(ev.timestamp)
assertEquals(aggId, ev.aggregateId)
assertEquals(EventType("TestEvent"), ev.eventType)
assertEquals(EventVersion(1), ev.version)
}
@Test
fun `primary constructor uses provided id and timestamp`() {
val aggId = AggregateId(uuid4())
val eid = EventId(uuid4())
val ts = kotlin.time.Instant.parse("2025-01-01T00:00:00Z")
val base = object : BaseDomainEvent(
aggregateId = aggId,
eventType = EventType("TestEvent"),
version = EventVersion(2),
eventId = eid,
timestamp = ts,
correlationId = CorrelationId(uuid4()),
causationId = CausationId(uuid4())
) {}
assertEquals(eid, base.eventId)
assertEquals(ts, base.timestamp)
assertEquals(EventVersion(2), base.version)
}
}
@@ -0,0 +1,54 @@
package at.mocode.core.domain
import at.mocode.core.domain.serialization.*
import com.benasher44.uuid.uuid4
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
@OptIn(kotlin.time.ExperimentalTime::class)
class SerializersTest {
@Test
fun `Instant roundtrip`() {
val instant = kotlin.time.Instant.parse("2024-01-01T00:00:00Z")
val json = Json.encodeToString(KotlinInstantSerializer, instant)
val decoded = Json.decodeFromString(KotlinInstantSerializer, json)
assertEquals(instant, decoded)
}
@Test
fun `UUID roundtrip`() {
val uuid = uuid4()
val json = Json.encodeToString(UuidSerializer, uuid)
val decoded = Json.decodeFromString(UuidSerializer, json)
assertEquals(uuid, decoded)
}
@Test
fun `LocalDate roundtrip`() {
val ld = LocalDate.parse("2024-06-15")
val json = Json.encodeToString(LocalDateSerializer, ld)
val decoded = Json.decodeFromString(LocalDateSerializer, json)
assertEquals(ld, decoded)
}
@Test
fun `LocalDateTime roundtrip`() {
val ldt = LocalDateTime.parse("2024-06-15T12:34:56")
val json = Json.encodeToString(LocalDateTimeSerializer, ldt)
val decoded = Json.decodeFromString(LocalDateTimeSerializer, json)
assertEquals(ldt, decoded)
}
@Test
fun `LocalTime roundtrip`() {
val lt = LocalTime.parse("12:34:56")
val json = Json.encodeToString(LocalTimeSerializer, lt)
val decoded = Json.decodeFromString(LocalTimeSerializer, json)
assertEquals(lt, decoded)
}
}
@@ -0,0 +1,46 @@
package at.mocode.core.domain
import at.mocode.core.domain.model.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class ValueTypesTest {
@Test
fun `EventType validation works`() {
assertFailsWith<IllegalArgumentException> { EventType("") }
assertFailsWith<IllegalArgumentException> { EventType("1Bad") }
assertFailsWith<IllegalArgumentException> { EventType("bad-char!") }
assertEquals("OrderCreated", EventType("OrderCreated").toString())
}
@Test
fun `EventVersion must be non-negative and comparable`() {
assertFailsWith<IllegalArgumentException> { EventVersion(-1) }
assertEquals(0, EventVersion(0).compareTo(EventVersion(0)))
assertTrue(EventVersion(2) > EventVersion(1))
}
@Test
fun `ErrorCode must be uppercase with allowed characters`() {
assertFailsWith<IllegalArgumentException> { ErrorCode("") }
assertFailsWith<IllegalArgumentException> { ErrorCode("abc") }
assertFailsWith<IllegalArgumentException> { ErrorCode("Bad_Code") }
assertEquals("VALID_CODE1", ErrorCode("VALID_CODE1").toString())
}
@Test
fun `PageNumber must be non-negative`() {
assertFailsWith<IllegalArgumentException> { PageNumber(-1) }
assertEquals("0", PageNumber(0).toString())
}
@Test
fun `PageSize range is enforced`() {
assertFailsWith<IllegalArgumentException> { PageSize(0) }
assertFailsWith<IllegalArgumentException> { PageSize(1001) }
assertEquals("1000", PageSize(1000).toString())
}
}
@@ -1,15 +0,0 @@
package at.mocode.core.domain.model
import kotlinx.serialization.Serializable
/**
* 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,
IMPORT_ZNS,
SYSTEM_GENERATED,
IMPORT_API
}
@@ -1,37 +0,0 @@
package at.mocode.core.utils.error
/**
* A functional approach to error handling, avoiding exceptions for predictable errors.
* Represents a value that can either be a Success (containing the result) or a Failure (containing an error).
*
* @param T The type of the success value.
* @param E The type of the error value.
*/
sealed class Result<out T, out E> {
data class Success<out T>(val value: T) : Result<T, Nothing>()
data class Failure<out E>(val error: E) : Result<Nothing, E>()
val isSuccess: Boolean get() = this is Success
val isFailure: Boolean get() = this is Failure
fun getOrNull(): T? = when (this) {
is Success -> value
is Failure -> null
}
fun getOrElse(defaultValue: @UnsafeVariance T): T = when (this) {
is Success -> value
is Failure -> defaultValue
}
}
// Extension functions for convenient usage
inline fun <T, E> Result<T, E>.onSuccess(action: (T) -> Unit): Result<T, E> {
if (this is Result.Success) action(value)
return this
}
inline fun <T, E> Result<T, E>.onFailure(action: (E) -> Unit): Result<T, E> {
if (this is Result.Failure) action(error)
return this
}
@@ -1,48 +0,0 @@
package at.mocode.core.utils.validation
/**
* Represents a single validation error.
* @param field The name of the field that failed validation.
* @param message A user-friendly error message.
* @param code A machine-readable error code for the client.
*/
data class ValidationError(
val field: String,
val message: String,
val code: String? = null
)
/**
* Represents the result of a validation process as a sealed class.
* This ensures that a result is either Valid or Invalid, but never both.
*/
sealed class ValidationResult {
/**
* Represents a successful validation.
*/
object Valid : ValidationResult()
/**
* Represents a failed validation with a list of specific errors.
*/
data class Invalid(val errors: List<ValidationError>) : ValidationResult()
fun isValid(): Boolean = this is Valid
fun isInvalid(): Boolean = this is Invalid
companion object {
fun invalid(field: String, message: String, code: String? = null): Invalid {
return Invalid(listOf(ValidationError(field, message, code)))
}
}
}
/**
* An exception that can be thrown to represent validation failure,
* allowing it to be caught by centralized error handling (like Ktor StatusPages).
*/
class ValidationException(
val validationResult: ValidationResult.Invalid
) : IllegalArgumentException(
"Validation failed: ${validationResult.errors.joinToString { "${it.field}: ${it.message}" }}"
)
@@ -1,67 +0,0 @@
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")
}
}
@@ -1,52 +0,0 @@
package at.mocode.core.domain
import at.mocode.core.domain.event.BaseDomainEvent
import at.mocode.core.domain.model.*
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: AggregateId = AggregateId(uuid4()),
@Transient
override val version: EventVersion = EventVersion(1L),
val testPayload: String = "Test"
) : BaseDomainEvent(
aggregateId = aggregateId,
eventType = EventType("TestEventOccurred"), // Ein klar definierter Event-Typ
version = version
)
@Test
fun `BaseDomainEvent should auto-generate eventId and timestamp upon creation`() {
// Arrange
val aggregateId = AggregateId(uuid4())
val version = EventVersion(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(EventType("TestEventOccurred"), event.eventType, "eventType should be set correctly")
}
}
@@ -1,78 +0,0 @@
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")
}
}