diff --git a/server/src/main/kotlin/dev/ulfrx/recipe/Application.kt b/server/src/main/kotlin/dev/ulfrx/recipe/Application.kt index daab354..8687ae3 100644 --- a/server/src/main/kotlin/dev/ulfrx/recipe/Application.kt +++ b/server/src/main/kotlin/dev/ulfrx/recipe/Application.kt @@ -1,20 +1,36 @@ package dev.ulfrx.recipe -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* -import io.ktor.server.response.* -import io.ktor.server.routing.* +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.response.respond +import io.ktor.server.routing.get +import io.ktor.server.routing.routing +import kotlinx.serialization.Serializable fun main() { embeddedServer(Netty, port = SERVER_PORT, host = "0.0.0.0", module = Application::module) .start(wait = true) } +@Serializable +private data class Health(val status: String) + fun Application.module() { + install(ContentNegotiation) { + json() + } + Database.migrate(this) + configureRouting() +} + +fun Application.configureRouting() { routing { - get("/") { - call.respondText("Ktor: ${Greeting().greet()}") + get("/health") { + call.respond(Health(status = "ok")) } } -} \ No newline at end of file +} diff --git a/server/src/main/kotlin/dev/ulfrx/recipe/Database.kt b/server/src/main/kotlin/dev/ulfrx/recipe/Database.kt new file mode 100644 index 0000000..f7cde44 --- /dev/null +++ b/server/src/main/kotlin/dev/ulfrx/recipe/Database.kt @@ -0,0 +1,31 @@ +package dev.ulfrx.recipe + +import io.ktor.server.application.Application +import org.flywaydb.core.Flyway +import org.slf4j.LoggerFactory + +object Database { + private val log = LoggerFactory.getLogger(Database::class.java) + + fun migrate(app: Application) { + val url = app.environment.config.property("database.url").getString() + val user = app.environment.config.property("database.user").getString() + val password = app.environment.config.property("database.password").getString() + + log.info("Connecting to {} as {} and running Flyway migrations", url, user) + + runCatching { + Flyway.configure() + .dataSource(url, user, password) + .locations("classpath:db/migration") + .baselineOnMigrate(true) + .validateOnMigrate(true) + .cleanDisabled(true) + .load() + .migrate() + }.onFailure { ex -> + log.error("Flyway migration failed — cannot start server", ex) + throw IllegalStateException("Database unreachable or migration failed", ex) + } + } +} diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..c82668d --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,18 @@ +ktor { + deployment { + port = 8080 + port = ${?PORT} + } + application { + modules = [ dev.ulfrx.recipe.ApplicationKt.module ] + } +} + +database { + url = "jdbc:postgresql://localhost:5432/recipe" + url = ${?DATABASE_URL} + user = "recipe" + user = ${?DATABASE_USER} + password = "recipe" + password = ${?DATABASE_PASSWORD} +} diff --git a/server/src/main/resources/db/migration/.gitkeep b/server/src/main/resources/db/migration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt b/server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt index 296e95f..6f9f71a 100644 --- a/server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt +++ b/server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt @@ -1,20 +1,30 @@ package dev.ulfrx.recipe -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.testing.* -import kotlin.test.* +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.install +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.testing.testApplication +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class ApplicationTest { @Test - fun testRoot() = testApplication { + fun `health endpoint returns 200 with status ok`() = testApplication { application { - module() + install(ContentNegotiation) { + json() + } + configureRouting() } - val response = client.get("/") + val response = client.get("/health") assertEquals(HttpStatusCode.OK, response.status) - assertEquals("Ktor: ${Greeting().greet()}", response.bodyAsText()) + val body = response.bodyAsText() + assertTrue(body.contains("\"status\""), "expected body to contain status field, was: $body") + assertTrue(body.contains("\"ok\""), "expected body to contain ok value, was: $body") } -} \ No newline at end of file +}