(fix) Server-Modul

This commit is contained in:
Stefan Mogeritsch 2025-06-29 23:34:04 +02:00
parent 89b8900fb2
commit 2ad447c978
27 changed files with 321 additions and 98 deletions

View File

@ -22,7 +22,7 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
// Configure application
application {
mainClass.set("at.mocode.server.ApplicationKt")
mainClass.set("at.mocode.ApplicationKt")
applicationDefaultJvmArgs = listOf(
"-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}",
"-XX:+UseG1GC", // Use G1 Garbage Collector
@ -75,7 +75,7 @@ dependencies {
// Testing
testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.kotlin.test)
testImplementation(libs.junitJupiter)
}

View File

@ -1,6 +1,7 @@
package at.mocode.server
package at.mocode
import at.mocode.server.plugins.configureDatabase
import at.mocode.plugins.configureDatabase
import at.mocode.plugins.configureRouting
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
@ -11,7 +12,6 @@ import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.json.Json
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
@ -22,18 +22,10 @@ fun main(args: Array<String>) {
fun Application.module() {
val log = LoggerFactory.getLogger("Application")
log.info("Initializing application...")
// Configure database
configureDatabase()
// Configure plugins
configurePlugins()
// Configure routing
configureRouting()
log.info("Application initialized successfully")
}
@ -93,7 +85,7 @@ private fun Application.configurePlugins() {
}
}
} catch (e: Exception) {
// Log the error but continue with default configuration
// Log the error but continue with the default configuration
this@configurePlugins.log.warn("Failed to configure CORS from config, using defaults: ${e.message}")
}
}
@ -115,28 +107,3 @@ private fun Application.configurePlugins() {
}
}
}
/**
* Configures all routes for the application
*/
private fun Application.configureRouting() {
routing {
// Health check endpoint
get("/health") {
call.respondText("OK")
}
// Root endpoint with basic information
get("/") {
// Read application info from config if available
val appName = application.environment.config.propertyOrNull("application.name")?.getString() ?: "Meldestelle API Server"
val appVersion = application.environment.config.propertyOrNull("application.version")?.getString() ?: "1.0.0"
val appEnv = application.environment.config.propertyOrNull("application.environment")?.getString() ?: "development"
call.respondText("$appName v$appVersion - Running in $appEnv mode")
}
// API routes can be organized in separate files and included here
// Example: registerUserRoutes()
}
}

View File

@ -0,0 +1,2 @@
package at.mocode.db

View File

@ -0,0 +1,2 @@
package at.mocode.model

View File

@ -0,0 +1,4 @@
package at.mocode.model
interface EventRepository {
}

View File

@ -0,0 +1,4 @@
package at.mocode.model
class PostgresEventRepository {
}

View File

@ -1,6 +1,5 @@
package at.mocode.server.plugins
package at.mocode.plugins
import at.mocode.server.tables.*
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.server.application.*
@ -175,14 +174,14 @@ private fun initializeSchema(log: Logger, isTestEnvironment: Boolean, isIdeaEnvi
try {
// Create all tables if they don't exist
SchemaUtils.create(
VereineTable,
PersonenTable,
PferdeTable,
VeranstaltungenTable,
TurniereTable,
ArtikelTable,
PlaetzeTable,
LizenzenTable
_root_ide_package_.at.mocode.tables.VereineTable,
_root_ide_package_.at.mocode.tables.PersonenTable,
_root_ide_package_.at.mocode.tables.PferdeTable,
_root_ide_package_.at.mocode.tables.VeranstaltungenTable,
_root_ide_package_.at.mocode.tables.TurniereTable,
_root_ide_package_.at.mocode.tables.ArtikelTable,
_root_ide_package_.at.mocode.tables.PlaetzeTable,
_root_ide_package_.at.mocode.tables.LizenzenTable
// Add more tables here if needed
)
log.info("Database schema initialized successfully.")

View File

@ -0,0 +1,32 @@
package at.mocode.plugins
import io.ktor.server.application.Application
import io.ktor.server.response.respondText
import io.ktor.server.routing.application
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
/**
* Configures all routes for the application
*/
fun Application.configureRouting() {
routing {
// Health check endpoint
get("/health") {
call.respondText("OK")
}
// Root endpoint with basic information
get("/") {
// Read application info from config if available
val appName = application.environment.config.propertyOrNull("application.name")?.getString() ?: "Meldestelle API Server"
val appVersion = application.environment.config.propertyOrNull("application.version")?.getString() ?: "1.0.0"
val appEnv = application.environment.config.propertyOrNull("application.environment")?.getString() ?: "development"
call.respondText("$appName v$appVersion - Running in $appEnv mode")
}
// API routes can be organized in separate files and included here
// Example: registerUserRoutes()
}
}

View File

@ -0,0 +1,2 @@
package at.mocode.plugins

View File

@ -1,4 +1,4 @@
package at.mocode.server.tables
package at.mocode.tables
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp

View File

@ -1,16 +1,17 @@
package at.mocode.server.tables
package at.mocode.tables
import at.mocode.shared.model.enums.LizenzTyp
import at.mocode.shared.model.enums.Sparte
import at.mocode.shared.enums.LizenzTypE
import at.mocode.shared.enums.SparteE
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date
object LizenzenTable : Table("lizenzen") {
val id = uuid("id")
val personId = uuid("person_id").references(PersonenTable.id)
val lizenzTyp = enumerationByName("lizenz_typ", 50, LizenzTyp::class)
val lizenzTyp = enumerationByName("lizenz_typ", 50, LizenzTypE::class)
val stufe = varchar("stufe", 20).nullable()
val sparte = enumerationByName("sparte", 50, Sparte::class).nullable()
val sparte = enumerationByName("sparte", 50, SparteE::class).nullable()
val gueltigBisJahr = integer("gueltig_bis_jahr").nullable()
val ausgestelltAm = date("ausgestellt_am").nullable()

View File

@ -1,6 +1,6 @@
package at.mocode.server.tables
package at.mocode.tables
import at.mocode.shared.model.enums.Geschlecht
import at.mocode.shared.enums.GeschlechtE
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
@ -12,7 +12,7 @@ object PersonenTable : Table("personen") {
val vorname = varchar("vorname", 100)
val titel = varchar("titel", 50).nullable()
val geburtsdatum = date("geburtsdatum").nullable()
val geschlecht = enumerationByName("geschlecht", 10, Geschlecht::class).nullable()
val geschlecht = enumerationByName("geschlecht", 10, GeschlechtE::class).nullable()
val nationalitaet = varchar("nationalitaet", 3).nullable()
val email = varchar("email", 255).nullable()
val telefon = varchar("telefon", 50).nullable()

View File

@ -1,6 +1,6 @@
package at.mocode.server.tables
package at.mocode.tables
import at.mocode.shared.model.enums.GeschlechtPferd
import at.mocode.shared.enums.GeschlechtPferdE
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
@ -11,7 +11,7 @@ object PferdeTable : Table("pferde") {
val name = varchar("name", 255)
val lebensnummer = varchar("lebensnummer", 20).nullable()
val feiPassNr = varchar("fei_pass_nr", 20).nullable()
val geschlecht = enumerationByName("geschlecht", 10, GeschlechtPferd::class).nullable()
val geschlecht = enumerationByName("geschlecht", 10, GeschlechtPferdE::class).nullable()
val geburtsjahr = integer("geburtsjahr").nullable()
val rasse = varchar("rasse", 100).nullable()
val farbe = varchar("farbe", 50).nullable()

View File

@ -1,6 +1,6 @@
package at.mocode.server.tables
package at.mocode.tables
import at.mocode.shared.model.enums.PlatzTyp
import at.mocode.shared.enums.PlatzTypE
import org.jetbrains.exposed.sql.Table
object PlaetzeTable : Table("plaetze") {
@ -9,7 +9,7 @@ object PlaetzeTable : Table("plaetze") {
val name = varchar("name", 100)
val dimension = varchar("dimension", 50).nullable()
val boden = varchar("boden", 100).nullable()
val typ = enumerationByName("typ", 20, PlatzTyp::class)
val typ = enumerationByName("typ", 20, PlatzTypE::class)
override val primaryKey = PrimaryKey(id)

View File

@ -1,4 +1,4 @@
package at.mocode.server.tables
package at.mocode.tables
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date // Für kotlinx-datetime LocalDate

View File

@ -1,6 +1,6 @@
package at.mocode.server.tables
package at.mocode.tables
import at.mocode.shared.model.enums.VeranstalterTyp
import at.mocode.shared.enums.VeranstalterTypE
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
@ -13,8 +13,8 @@ object VeranstaltungenTable : Table("veranstaltungen") {
val veranstalterName = varchar("veranstalter_name", 255)
val veranstalterOepsNummer = varchar("veranstalter_oeps_nr", 10).nullable()
val veranstalterTyp =
enumerationByName("veranstalter_typ", 20, VeranstalterTyp::class).default(
VeranstalterTyp.UNBEKANNT
enumerationByName("veranstalter_typ", 20, VeranstalterTypE::class).default(
VeranstalterTypE.UNBEKANNT
)
val veranstaltungsortName = varchar("veranstaltungsort_name", 255)
val veranstaltungsortAdresse = varchar("veranstaltungsort_adresse", 500)

View File

@ -1,4 +1,4 @@
package at.mocode.server.tables
package at.mocode.tables
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp

View File

@ -2,7 +2,7 @@
ktor:
deployment:
# Server port configuration
port: 8081
port: 8080
# Connection timeout in seconds
connectionTimeout: 30
# Maximum number of concurrent connections
@ -13,7 +13,7 @@ ktor:
- resources
application:
modules:
- at.mocode.server.ApplicationKt.module
- at.mocode.ApplicationKt.module
# Database Configuration
database:
@ -40,8 +40,8 @@ security:
issuer: "meldestelle-server"
audience: "meldestelle-clients"
realm: "meldestelle"
# Secret should be set via environment variable in production
secret: "${JWT_SECRET:dev-secret-key-change-in-production"
# Secret should be set via an environment variable in production
secret: "${JWT_SECRET:dev-secret-key-change-in-production}"
# Token validity duration in milliseconds (24 hours)
validity: 86400000
@ -52,13 +52,6 @@ cors:
- "localhost:3000"
- "127.0.0.1:3000"
- "meldestelle.mocode.at"
# Allow these HTTP methods
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
# Allow credentials (cookies, auth headers)
allowCredentials: true

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,166 @@
package at.mocode
import at.mocode.plugins.configureRouting
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import kotlinx.serialization.json.Json
import kotlin.test.*
/**
* Comprehensive test suite for the Meldestelle Server Application.
*
* This test class verifies:
* - Application startup and initialization
* - Core routing functionality (health check, root endpoint)
* - Plugin configuration (CORS, content negotiation, default headers)
* - Error handling
* - Basic HTTP functionality
*/
class ApplicationTest {
companion object {
init {
// Set test environment property for database configuration
// This ensures the application uses H2 in-memory database for testing
System.setProperty("isTestEnvironment", "true")
}
}
@Test
fun testApplicationStartup() = testApplication {
application {
module()
}
// Test that the application starts without errors
// This test passes if no exceptions are thrown during module initialization
}
@Test
fun testHealthEndpoint() = testApplication {
application {
module()
}
client.get("/health").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("OK", bodyAsText())
}
}
@Test
fun testRootEndpoint() = testApplication {
application {
module()
}
client.get("/").apply {
assertEquals(HttpStatusCode.OK, status)
val responseText = bodyAsText()
// The response format is: "Meldestelle API Server v1.0.0 - Running in development mode"
assertTrue(responseText.contains("Meldestelle API Server"), "Response should contain 'Meldestelle API Server', but was: $responseText")
assertTrue(responseText.contains("v1.0.0"), "Response should contain 'v1.0.0', but was: $responseText")
assertTrue(responseText.contains("development"), "Response should contain 'development', but was: $responseText")
}
}
@Test
fun testNotFoundEndpoint() = testApplication {
application {
module()
}
client.get("/nonexistent").apply {
assertEquals(HttpStatusCode.NotFound, status)
val responseText = bodyAsText()
assertTrue(responseText.contains("404: Page Not Found"))
}
}
@Test
fun testDefaultHeaders() = testApplication {
application {
module()
}
client.get("/health").apply {
assertEquals(HttpStatusCode.OK, status)
// Check that default headers are set
assertEquals("Ktor", headers["X-Engine"])
assertEquals("nosniff", headers["X-Content-Type-Options"])
}
}
@Test
fun testCorsConfiguration() = testApplication {
application {
module()
}
client.get("/health") {
header(HttpHeaders.Origin, "http://localhost:3000")
}.apply {
assertEquals(HttpStatusCode.OK, status)
// Check that CORS headers are present
assertNotNull(headers[HttpHeaders.AccessControlAllowOrigin])
}
}
@Test
fun testContentNegotiation() = testApplication {
application {
module()
}
client.get("/health") {
header(HttpHeaders.Accept, "application/json")
}.apply {
assertEquals(HttpStatusCode.OK, status)
// The response should still be text for the health endpoint
assertEquals("OK", bodyAsText())
}
}
@Test
fun testOptionsRequest() = testApplication {
application {
module()
}
client.options("/health") {
header(HttpHeaders.Origin, "http://localhost:3000")
header(HttpHeaders.AccessControlRequestMethod, "GET")
}.apply {
// OPTIONS requests should be handled by CORS
assertTrue(status.isSuccess() || status == HttpStatusCode.NotFound)
}
}
@Test
fun testBasicRoutingWithoutDatabase() = testApplication {
application {
// Test routing functionality without full application module
// This isolates routing from database dependencies
install(DefaultHeaders) {
header("X-Engine", "Ktor")
header("X-Content-Type-Options", "nosniff")
}
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
configureRouting()
}
client.get("/health").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals("OK", bodyAsText())
}
}
}

View File

@ -0,0 +1,10 @@
package at.mocode
import kotlin.test.*
class SimpleTest {
@Test
fun testBasic() {
assertEquals(1, 1)
}
}

View File

@ -51,14 +51,5 @@ kotlin {
implementation(kotlin("test"))
}
}
// val jvmMain by getting {
// dependsOn(commonMain)
// }
//
// val wasmJsMain by getting {
// dependsOn(commonMain)
// }
}
}

View File

@ -0,0 +1,41 @@
package at.mocode.model
import kotlinx.serialization.Serializable
/**
* Represents a tournament (Turnier) with its details and associated competitions (Bewerbe).
* Each tournament can have one or more competitions.
*/
@Serializable
data class Turnier(
/** The name of the tournament, e.g. "CSN-C NEU CSNP-C NEU NEUMARKT/M., OÖ" */
val name: String,
/** The date of the tournament as a formatted string, e.g. "7.JUNI 2025" */
val datum: String,
/** Unique identifier for the tournament */
val number: Int,
/** List of competitions (Bewerbe) associated with this tournament */
var bewerbe: List<Bewerb>
)
/**
* Represents a competition (Bewerb) within a tournament.
* A competition has specific details like number, title, class, and optional task.
*/
@Serializable
data class Bewerb(
/** Competition number, e.g. 1, 2, etc. */
val nummer: Int,
/** Title of the competition, e.g. "Stilspringprüfung" or "Dressurprüfung" */
val titel: String,
/** Class/level of the competition, e.g. "60 cm" or "Kl. A" */
val klasse: String,
/** Optional task identifier, e.g. "DRA 1" */
val task: String?
)

View File

@ -18,7 +18,7 @@ enum class CupSerieTypE { CUP_SERIE }
@Serializable
enum class LizenzKategorieE { REITERLIZENZ, FAHRERLIZENZ, STARTKARTE }
@Serializable
enum class LizenzTyp { REITER, FAHRER, VOLTIGIERER, WESTERN, WORKING_EQUITATION, POLO, STARTKARTE_ALLG, STARTKARTE_VOLTIGIEREN, STARTKARTE_WESTERN, STARTKARTE_ISLAND, STARTKARTE_FAHREN_JUGEND, STARTKARTE_HORSEBALL, STARTKARTE_POLO, PARAEQUESTRIAN, SONSTIGE }
enum class LizenzTypE { REITER, FAHRER, VOLTIGIERER, WESTERN, WORKING_EQUITATION, POLO, STARTKARTE_ALLG, STARTKARTE_VOLTIGIEREN, STARTKARTE_WESTERN, STARTKARTE_ISLAND, STARTKARTE_FAHREN_JUGEND, STARTKARTE_HORSEBALL, STARTKARTE_POLO, PARAEQUESTRIAN, SONSTIGE }
@Serializable
@ -70,4 +70,4 @@ enum class RichterPositionE { C, E, H, M, B, VORSITZ, SEITENRICHTER, SONSTIGE }
@Serializable
enum class GeschlechtE { M, W, D, UNBEKANNT }
@Serializable
enum class GeschlechtPferd { HENGST, STUTE, WALLACH, UNBEKANNT }
enum class GeschlechtPferdE { HENGST, STUTE, WALLACH, UNBEKANNT }

View File

@ -1,7 +1,6 @@
package at.mocode.shared.model
import at.mocode.shared.model.Artikel
import at.mocode.shared.enums.NennungsArt
import at.mocode.shared.enums.NennungsArtE
import at.mocode.shared.serializers.BigDecimalSerializer
import at.mocode.shared.serializers.KotlinInstantSerializer
import at.mocode.shared.serializers.KotlinLocalDateSerializer

View File

@ -1,6 +1,6 @@
package at.mocode.shared.stammdaten
import at.mocode.shared.enums.LizenzTyp
import at.mocode.shared.enums.LizenzTypE
import at.mocode.shared.enums.SparteE
import at.mocode.shared.serializers.KotlinLocalDateSerializer
import kotlinx.datetime.LocalDate
@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class LizenzInfo(
val lizenzTyp: LizenzTyp,
val lizenzTyp: LizenzTypE,
val stufe: String?,
val sparteE: SparteE?,
val gueltigBisJahr: Int?, // Jahr als Int

View File

@ -1,6 +1,6 @@
package at.mocode.shared.stammdaten
import at.mocode.shared.enums.GeschlechtPferd
import at.mocode.shared.enums.GeschlechtPferdE
import at.mocode.shared.serializers.KotlinInstantSerializer
import at.mocode.shared.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
@ -18,7 +18,7 @@ data class Pferd(
var name: String,
var lebensnummer: String?,
var feiPassNr: String?,
var geschlecht: GeschlechtPferd?,
var geschlecht: GeschlechtPferdE?,
var geburtsjahr: Int?,
var rasse: String?,
var farbe: String?,