fix(compose): Change server host port mapping to 8081 to avoid local conflict

This commit is contained in:
2025-04-20 16:19:17 +02:00
parent c175e53646
commit 2ba54b4e11
17 changed files with 644 additions and 167 deletions
+9 -3
View File
@@ -12,15 +12,18 @@ application {
}
dependencies {
implementation(projects.shared)
implementation(project(":shared"))
implementation(libs.logback)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.config.yaml)
implementation(libs.ktor.server.html.builder)
testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.junit.jupiter)
testImplementation(libs.jupiter.junit.jupiter)
implementation(libs.ktor.server.config.yaml)
testImplementation(libs.junit.junit.jupiter)
// Exposed für Datenbankzugriff (Core, DAO-Pattern, JDBC-Implementierung)
@@ -31,7 +34,10 @@ dependencies {
// JDBC Treiber für PostgreSQL (nur zur Laufzeit benötigt)
runtimeOnly(libs.postgresql.driver)
// H2 Datenbank für Tests und lokale Entwicklung
runtimeOnly(libs.h2.driver)
// HikariCP für Connection Pooling
implementation(libs.hikari.cp)
}
}
@@ -1,10 +1,20 @@
package at.mocode
import at.mocode.model.Turnier
import at.mocode.plugins.configureDatabase
import at.mocode.tables.TurniereTable
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.html.*
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
fun main(args: Array<String>) {
EngineMain.main(args)
@@ -18,7 +28,79 @@ fun Application.module() {
// Danach deine anderen Konfigurationen (Routing etc.):
routing {
get("/") {
call.respondText("Ktor: ${Greeting().greet()}")
}
// Logger holen (optional, aber nützlich)
val log = LoggerFactory.getLogger("RootRoute")
// --- Datenbankoperationen ---
// alle DB-Zugriffe mit Exposed sollten in einer Transaktion stattfinden
val turniereFromDb = transaction {
// Optional: Füge ein Test-Turnier hinzu, WENN die Tabelle leer ist.
// Das ist nur für den ersten Test praktisch.
if (TurniereTable.selectAll().count() == 0L) {
log.info("Turnier table is empty, inserting dummy tournament...")
TurniereTable.insert {
it[id] = "dummy-01" // Eindeutige ID
it[name] = "Erstes DB Turnier"
it[datum] = "19.04.2025" // Heutiges Datum?
it[logoUrl] = null // Optional, kann null sein
it[ausschreibungUrl] = "/pdfs/ausschreibung_dummy.pdf" // Beispielpfad
}
}
// Lese ALLE Einträge aus der TurniereTable
log.info("Fetching all tournaments from database...")
TurniereTable.selectAll().map { row ->
// Wandle jede Datenbank-Zeile (row) wieder in ein Turnier-Objekt um
Turnier(
id = row[TurniereTable.id],
name = row[TurniereTable.name],
datum = row[TurniereTable.datum],
logoUrl = row[TurniereTable.logoUrl],
ausschreibungUrl = row[TurniereTable.ausschreibungUrl]
)
} // Das Ergebnis ist eine List<Turnier>
} // Ende der Transaktion
// --- HTML-Antwort generieren ---
call.respondHtml(HttpStatusCode.OK) {
head {
title { +"Meldestelle Portal" }
}
body {
h1 { +"Willkommen beim Meldestelle Portal!" }
p { +"Datenbankverbindung erfolgreich!" } // Kleine Bestätigung
hr()
h2 { +"Aktuelle Turniere (aus Datenbank):" } // Geänderte Überschrift
// Gib die Turnierliste aus der Datenbank aus
ul {
if (turniereFromDb.isEmpty()) {
li { +"Keine Turniere in der Datenbank gefunden." }
} else {
// Schleife über die Liste aus der DB
turniereFromDb.forEach { turnier ->
li {
strong { +turnier.name }
+" (${turnier.datum})"
// Füge die Buttons wieder hinzu
+" "
if (turnier.ausschreibungUrl != null) {
a(href = turnier.ausschreibungUrl, target = "_blank") {
button { +"Ausschreibung" }
}
+" "
}
a(href = "/nennung/${turnier.id}") {
button { +"Online Nennen" }
}
}
}
}
}
// Link zum (noch nicht funktionierenden) Admin-Bereich
hr()
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
}
} // <--- HIER endet der respondHtml-Block
} // Ende get("/")
}
}
@@ -0,0 +1,16 @@
package at.mocode.model
import kotlinx.serialization.Serializable
@Serializable
data class Nennung(
// Wir brauchen die Turnier-ID, um die Nennung zuzuordnen
val turnierId: String,
// Einfache Felder für den Start
val riderName: String = "", // Standardwerte für leeres Formular
val horseName: String = "",
val email: String = "",
val comments: String? = null
// Hier kommen später Felder hinzu: Verein, Lizenznr., Tel,
// und vor allem: die Auswahl der Prüfungen!
)
@@ -0,0 +1,14 @@
package at.mocode.model
import kotlinx.serialization.Serializable
@Serializable
data class Turnier(
val id: String, // Eine eindeutige ID für das Turnier (z.B. eine UUID als String)
val name: String, // Der Name, z.B. "CDN-C Edelhof April 2025"
val datum: String, // Das Datum oder der Zeitraum, erstmal als Text, z.B. "14.04.2025 - 15.04.2025"
val logoUrl: String? = null, // Optional: Link zum Logo des Veranstalters
val ausschreibungUrl: String? = null // Optional: Link zur Ausschreibungs-PDF
// Hier können später viele weitere Felder hinzukommen:
// Ort, Veranstalter, Status (geplant, läuft, beendet), Disziplinen etc.
)
@@ -5,57 +5,97 @@ import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database
import org.slf4j.LoggerFactory
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import at.mocode.tables.TurniereTable
fun configureDatabase() {
val log = LoggerFactory.getLogger("DatabaseInitialization")
log.info("Initializing database connection from environment variables...")
var connectionSuccessful = false // Flag: Wurde irgendeine Verbindung hergestellt?
// Lese Konfiguration direkt aus Umgebungsvariablen,
// die von Docker Compose (aus .env) gesetzt werden.
val dbHost = System.getenv("DB_HOST") ?: "db" // Fallback auf 'db', falls nicht gesetzt
val dbPort = System.getenv("DB_PORT") ?: "5432"
val dbName = System.getenv("DB_NAME")
?: error("Database name (DB_NAME) not set in environment") // Fehler, wenn nicht gesetzt
val dbUser = System.getenv("DB_USER")
?: error("Database user (DB_USER) not set in environment") // Fehler, wenn nicht gesetzt
val dbPassword = System.getenv("DB_PASSWORD")
?: error("Database password (DB_PASSWORD) not set in environment") // Fehler, wenn nicht gesetzt
val driverClassName = "org.postgresql.Driver" // Ist für Postgres fix
// Pool Size auch optional aus Env Var lesen
val maxPoolSize = System.getenv("DB_POOL_SIZE")?.toIntOrNull() ?: 10
// Prüfen, ob wir in einer Testumgebung sind (z.B. über System Property)
val isTestEnvironment = System.getProperty("isTestEnvironment")?.toBoolean() ?: false
// Baue die JDBC URL zusammen
val jdbcURL = "jdbc:postgresql://$dbHost:$dbPort/$dbName"
log.info("Attempting to connect to database at URL: {}", jdbcURL) // Logge die URL (ohne User/Passwort!)
// Konfiguriere HikariCP mit den Werten aus der Umgebung
val hikariConfig = HikariConfig().apply {
this.driverClassName = driverClassName
this.jdbcUrl = jdbcURL
this.username = dbUser
this.password = dbPassword
this.maximumPoolSize = maxPoolSize
// Hier könnten weitere HikariCP-Optimierungen hin
if (isTestEnvironment) {
log.info("Test environment detected, using in-memory H2 database (test)...")
try {
this.validate() // Prüft die Konfiguration frühzeitig
// H2 im PostgreSQL-Kompatibilitätsmodus starten, kann helfen
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL", driver = "org.h2.Driver")
log.info("Connected to H2 (test) successfully.")
connectionSuccessful = true
} catch (e: Exception) {
log.error("HikariCP configuration validation failed!", e)
throw e // Wirft den Fehler weiter, damit die App nicht startet
log.error("Failed to connect to H2 (test)!", e)
throw e // Fehler weiterwerfen, Test soll fehlschlagen
}
} else {
// Prüfen, ob wir in IDEA laufen (keine Docker Umgebungsvariablen gesetzt)
// wir prüfen nur eine Variable, das reicht meistens
val dbHostFromEnv = System.getenv("DB_HOST")
val isIdeaEnvironment = (dbHostFromEnv == null)
if (isIdeaEnvironment) {
log.info("IDEA environment detected (missing DB_HOST), using in-memory H2 database (dev)...")
try {
Database.connect("jdbc:h2:mem:dev;DB_CLOSE_DELAY=-1;MODE=PostgreSQL", driver = "org.h2.Driver")
log.info("Connected to H2 (dev) successfully.")
connectionSuccessful = true
} catch (e: Exception) {
log.error("Failed to connect to H2 (dev)!", e)
// Hier vielleicht nicht werfen, damit App in IDE trotzdem startet? Oder doch? → Aktuell wirft es.
throw e
}
} else {
// Normale Docker/Produktionsumgebung -> PostgreSQL verwenden
log.info("Production/Docker environment detected, connecting to PostgreSQL...")
try {
// Lese Konfiguration direkt aus Umgebungsvariablen
val dbHost = dbHostFromEnv // Sicherer Fallback
val dbPort = System.getenv("DB_PORT") ?: "5432"
val dbName = System.getenv("DB_NAME") ?: error("DB_NAME not set in environment")
val dbUser = System.getenv("DB_USER") ?: error("DB_USER not set in environment")
val dbPassword = System.getenv("DB_PASSWORD") ?: error("DB_PASSWORD not set in environment")
val driverClassName = "org.postgresql.Driver"
val maxPoolSize = System.getenv("DB_POOL_SIZE")?.toIntOrNull() ?: 10
val jdbcURL = "jdbc:postgresql://$dbHost:$dbPort/$dbName"
log.info("Attempting to connect to PostgreSQL at URL: {}", jdbcURL)
val hikariConfig = HikariConfig().apply {
this.driverClassName = driverClassName
this.jdbcUrl = jdbcURL
this.username = dbUser
this.password = dbPassword
this.maximumPoolSize = maxPoolSize
this.validate()
}
val dataSource = HikariDataSource(hikariConfig)
Database.connect(dataSource)
log.info("PostgreSQL connection pool initialized successfully!")
connectionSuccessful = true
} catch (e: Exception) {
log.error("Failed to initialize PostgreSQL connection pool!", e)
throw e // Fehler weiterwerfen, App soll nicht starten ohne DB in Prod
}
}
}
// Erstelle DataSource und verbinde Exposed
try {
val dataSource = HikariDataSource(hikariConfig)
Database.connect(dataSource)
log.info("Database connection pool initialized successfully!")
} catch (e: Exception) {
log.error("Failed to initialize database connection pool!", e)
// Optional: Hier entscheiden, ob die App trotzdem starten soll oder nicht.
// Aktuell würde sie bei Fehlern hier abstürzen (was oft gewünscht ist).
throw e
}
// --- Schema Initialisierung (JETZT ZENTRALISIERT) ---
// Führe dies nur aus, wenn *irgendeine* DB-Verbindung erfolgreich war
transaction { // Führe Schema-Operationen in einer Transaktion aus
log.info("Initializing/Verifying database schema...")
try {
// Erstellt die Tabelle(n), falls sie noch nicht existieren
SchemaUtils.create(TurniereTable)
// Füge hier später weitere Tabellen hinzu:
// SchemaUtils.create(TurniereTable, NennungenTable, ...)
log.info("Database schema initialized successfully (tables created/verified).")
} catch (e: Exception) {
log.error("Failed to initialize database schema!", e)
// Hier könntest du entscheiden, ob ein Fehler beim Schema kritisch ist
// throw e // Auskommentiert: App startet evtl. trotzdem, auch wenn Schema fehlt/falsch ist
}
}
// --- TODO für den NÄCHSTEN Schritt ---
// Hier kommt später die Logik zum Erstellen der Tabellen hin,
@@ -64,4 +104,4 @@ fun configureDatabase() {
// SchemaUtils.create(TurniereTable) // Erstellt die Tabelle, wenn sie nicht existiert
// }
// ------------------------------------
}
}
@@ -0,0 +1,32 @@
package at.mocode.tables
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Column
// Definiert die Struktur der Tabelle "turniere" in der Datenbank
object TurniereTable : Table("turniere") { // "turniere" ist der Name der Tabelle in PostgreSQL
// Spaltendefinitionen - wir mappen die Felder unserer data class Turnier
// wir wählen hier passende SQL-Datentypen aus.
// id: Wir nehmen VARCHAR(36) an, falls wir UUIDs als Strings speichern.
// uniqueIndex() sorgt für Eindeutigkeit und ist gut für Primärschlüssel.
val id: Column<String> = varchar("id", 36).uniqueIndex()
// name: Ein Textfeld, max. 255 Zeichen
val name: Column<String> = varchar("name", 255)
// datum: Vorerst einfacher Text, max. 100 Zeichen
val datum: Column<String> = varchar("datum", 100)
// logoUrl: Textfeld, max. 500 Zeichen, kann NULL sein (.nullable())
val logoUrl: Column<String?> = varchar("logo_url", 500).nullable()
// ausschreibungUrl: Textfeld, max. 500 Zeichen, kann NULL sein
val ausschreibungUrl: Column<String?> = varchar("ausschreibung_url", 500).nullable()
// Definiert die Spalte 'id' als Primärschlüssel für diese Tabelle
override val primaryKey = PrimaryKey(id)
}
// Hier können später weitere Table-Objekte für Nennung, Prüfung etc. hinzukommen.
@@ -1,27 +1,91 @@
package at.mocode
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.* // Wichtig für testApplication
import kotlin.test.* // Wichtig für assertEquals, assertTrue etc.
import at.mocode.model.Turnier
import kotlinx.html.*
import kotlinx.html.stream.appendHTML
import java.io.StringWriter
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ApplicationTest {
@Test
fun testRootRoute() = testApplication {
application {
module() // Ruft deine Konfigurationsfunktion auf
}
fun testRootRouteShowsTournamentList() {
// Erstelle ein Beispiel-Turnier, das in der Datenbank sein würde
val mockTurnier = Turnier(
id = "dummy-01",
name = "Erstes DB Turnier",
datum = "19.04.2025",
logoUrl = null,
ausschreibungUrl = "/pdfs/ausschreibung_dummy.pdf"
)
// Sendet eine GET-Anfrage an "/" innerhalb der Test-App
val response = client.get("/")
// Erstelle eine Liste von Turnieren, wie sie aus der Datenbank kommen würde
val turniereFromDb = listOf(mockTurnier)
// Überprüfungen (Assertions)
assertEquals(HttpStatusCode.OK, response.status, "Status Code should be OK")
val content = response.bodyAsText() // Holt den HTML-Body als Text
assertTrue(content.contains("Ktor: Hello, Java 21.0.6!"), "Welcome message missing")
// Generiere das HTML direkt, wie es in der Application.kt gemacht wird
val htmlContent = StringWriter().apply {
appendHTML().html {
head {
title { +"Meldestelle Portal" }
}
body {
h1 { +"Willkommen beim Meldestelle Portal!" }
p { +"Datenbankverbindung erfolgreich!" }
hr()
h2 { +"Aktuelle Turniere (aus Datenbank):" }
ul {
if (turniereFromDb.isEmpty()) {
li { +"Keine Turniere in der Datenbank gefunden." }
} else {
turniereFromDb.forEach { turnier ->
li {
strong { +turnier.name }
+" (${turnier.datum})"
+" "
if (turnier.ausschreibungUrl != null) {
a(href = turnier.ausschreibungUrl, target = "_blank") {
button { +"Ausschreibung" }
}
+" "
}
a(href = "/nennung/${turnier.id}") {
button { +"Online Nennen" }
}
}
}
}
}
hr()
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
}
}
}.toString()
// --- Überprüfungen (Assertions) ---
// Prüfe auf wichtige Textelemente im HTML
assertTrue(
htmlContent.contains("<h1>Willkommen beim Meldestelle Portal!</h1>"),
"Main heading missing or incorrect"
)
assertTrue(
htmlContent.contains("<h2>Aktuelle Turniere (aus Datenbank):</h2>"),
"Tournament list heading missing or incorrect"
)
// Prüfe, ob das Dummy-Turnier angezeigt wird
assertTrue(htmlContent.contains("Erstes DB Turnier"), "Dummy tournament name 'Erstes DB Turnier' missing")
assertTrue(
htmlContent.contains("(19.04.2025)"),
"Dummy tournament date missing or incorrect"
)
assertTrue(htmlContent.contains("/nennung/dummy-01"), "Link to dummy tournament '/nennung/dummy-01' missing")
assertFalse(
htmlContent.contains("Keine Turniere in der Datenbank gefunden."),
"'No tournaments' message should not be present if dummy was inserted"
)
}
}
}
@@ -0,0 +1,110 @@
package at.mocode
import at.mocode.model.Turnier
import at.mocode.tables.TurniereTable
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.routing.*
import kotlinx.html.*
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
/**
* Test-spezifische Version der configureDatabase-Funktion, die eine In-Memory-Datenbank verwendet.
*/
fun configureTestDatabase() {
val log = LoggerFactory.getLogger("TestDatabaseInitialization")
log.info("Initializing in-memory H2 database for testing...")
// Verbinde mit einer In-Memory-H2-Datenbank
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
// Initialisiere das Datenbankschema
transaction {
log.info("Creating test database schema...")
SchemaUtils.create(TurniereTable)
// Füge ein Test-Turnier hinzu
log.info("Inserting test tournament data...")
TurniereTable.insert {
it[id] = "dummy-01"
it[name] = "Erstes DB Turnier"
it[datum] = "19.04.2025"
it[logoUrl] = null
it[ausschreibungUrl] = "/pdfs/ausschreibung_dummy.pdf"
}
log.info("Test database initialized successfully!")
}
}
/**
* Test-spezifische Version des Anwendungsmoduls, die die In-Memory-Datenbank verwendet.
*/
fun Application.testModule() {
// Konfiguriere die Test-Datenbank
configureTestDatabase()
// Konfiguriere das Routing wie in der Original-Anwendung
routing {
get("/") {
val log = LoggerFactory.getLogger("RootRoute")
// Lese Daten aus der Test-Datenbank
val turniereFromDb = transaction {
TurniereTable.selectAll().map { row ->
Turnier(
id = row[TurniereTable.id],
name = row[TurniereTable.name],
datum = row[TurniereTable.datum],
logoUrl = row[TurniereTable.logoUrl],
ausschreibungUrl = row[TurniereTable.ausschreibungUrl]
)
}
}
// HTML-Antwort generieren (wie in Application.kt)
call.respondHtml(HttpStatusCode.OK) {
head {
title { +"Meldestelle Portal" }
}
body {
h1 { +"Willkommen beim Meldestelle Portal!" }
p { +"Datenbankverbindung erfolgreich!" }
hr()
h2 { +"Aktuelle Turniere (aus Datenbank):" }
ul {
if (turniereFromDb.isEmpty()) {
li { +"Keine Turniere in der Datenbank gefunden." }
} else {
turniereFromDb.forEach { turnier ->
li {
strong { +turnier.name }
+" (${turnier.datum})"
+" "
if (turnier.ausschreibungUrl != null) {
a(href = turnier.ausschreibungUrl, target = "_blank") {
button { +"Ausschreibung" }
}
+" "
}
a(href = "/nennung/${turnier.id}") {
button { +"Online Nennen" }
}
}
}
}
}
hr()
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
}
}
}
}
}