💥 Nukes rust and replaces with kotlin
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
use rocket::Response;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome, Request};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::io::Cursor;
|
||||
|
||||
pub struct ApiKey {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GenericResponse {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for ApiKey {
|
||||
type Error = Response<'r>;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Response<'r>> {
|
||||
fn is_valid(key: &str) -> bool {
|
||||
key == dotenv::var("API_KEY").unwrap()
|
||||
}
|
||||
|
||||
match req.headers().get_one("Authorization") {
|
||||
None => {
|
||||
let body = json!(GenericResponse {
|
||||
message: "auth token not found".to_string()
|
||||
})
|
||||
.to_string();
|
||||
|
||||
Outcome::Error((
|
||||
Status::Unauthorized,
|
||||
Response::build()
|
||||
.status(Status::Unauthorized)
|
||||
.sized_body(body.len(), Cursor::new(body))
|
||||
.finalize(),
|
||||
))
|
||||
}
|
||||
Some(key) if is_valid(key) => Outcome::Success(ApiKey {}),
|
||||
Some(_) => {
|
||||
let body = json!(GenericResponse {
|
||||
message: "invalid auth token".to_string()
|
||||
})
|
||||
.to_string();
|
||||
|
||||
Outcome::Error((
|
||||
Status::Unauthorized,
|
||||
Response::build()
|
||||
.status(Status::Unauthorized)
|
||||
.sized_body(body.len(), Cursor::new(body))
|
||||
.finalize(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,83 +0,0 @@
|
||||
use rusqlite::{Connection, Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ActionKind {
|
||||
Text,
|
||||
Script,
|
||||
}
|
||||
|
||||
impl ActionKind {
|
||||
fn parse(kind: String) -> Result<ActionKind, Error> {
|
||||
match kind.to_lowercase().as_str() {
|
||||
"text" => Ok(ActionKind::Text),
|
||||
"script" => Ok(ActionKind::Script),
|
||||
_ => Err(Error::QueryReturnedNoRows),
|
||||
}
|
||||
}
|
||||
|
||||
fn stringify(&self) -> String {
|
||||
match self {
|
||||
ActionKind::Text => "text".to_string(),
|
||||
ActionKind::Script => "script".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Action {
|
||||
id: i32,
|
||||
name: String,
|
||||
kind: ActionKind,
|
||||
source: String,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
pub struct Conn {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl Conn {
|
||||
pub fn new(db_path: &str) -> Result<Conn, Error> {
|
||||
let conn = Connection::open(db_path).expect("failed to open database");
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE action (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)",
|
||||
(), // empty list of parameters.
|
||||
)?;
|
||||
|
||||
Ok(Conn { conn })
|
||||
}
|
||||
|
||||
fn action_by_id(self, id: i32) -> Result<Action, Error> {
|
||||
let mut query = self.conn.prepare("SELECT * FROM action WHERE id = ?")?;
|
||||
let mut actions = query.query_map(&[&id], |row| {
|
||||
Ok(Action {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
kind: ActionKind::parse(row.get(2)?)?,
|
||||
source: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
updated_at: row.get(5)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
let action = actions.next();
|
||||
let len = actions.next().unwrap().iter().count();
|
||||
if len != 0 {
|
||||
return Err(Error::InvalidQuery);
|
||||
}
|
||||
|
||||
if let None = action {
|
||||
return Err(Error::QueryReturnedNoRows);
|
||||
}
|
||||
action.unwrap()
|
||||
}
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
mod db;
|
||||
mod auth;
|
||||
|
||||
use dotenv;
|
||||
use auth::ApiKey;
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
#[get("/")]
|
||||
async fn index() -> &'static str {
|
||||
"Hello World!"
|
||||
}
|
||||
|
||||
#[get("/hi")]
|
||||
async fn hello(api_key: ApiKey) -> &'static str {
|
||||
"Hi!"
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
dotenv::dotenv().ok();
|
||||
let db_path = dotenv::var("DB_PATH").expect("DB_PATH is not set");
|
||||
dotenv::var("API_KEY").expect("API_KEY is not set");
|
||||
let db = db::Conn::new(&db_path);
|
||||
|
||||
rocket::build().mount("/", routes![index, hello])
|
||||
}
|
35
backend/src/main/kotlin/Administration.kt
Normal file
35
backend/src/main/kotlin/Administration.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.compression.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.requestvalidation.RequestValidation
|
||||
import io.ktor.server.plugins.requestvalidation.ValidationResult
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureAdministration() {
|
||||
routing {
|
||||
route("/") {
|
||||
install(RateLimiting) {
|
||||
rateLimiter {
|
||||
type = TokenBucket::class
|
||||
capacity = 100
|
||||
rate = 10.seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
backend/src/main/kotlin/Application.kt
Normal file
20
backend/src/main/kotlin/Application.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
|
||||
fun main() {
|
||||
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
|
||||
.start(wait = true)
|
||||
}
|
||||
|
||||
fun Application.module() {
|
||||
configureHTTP()
|
||||
configureSecurity()
|
||||
configureMonitoring()
|
||||
configureSerialization()
|
||||
configureDatabases()
|
||||
configureAdministration()
|
||||
configureRouting()
|
||||
}
|
65
backend/src/main/kotlin/Databases.kt
Normal file
65
backend/src/main/kotlin/Databases.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.compression.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.requestvalidation.RequestValidation
|
||||
import io.ktor.server.plugins.requestvalidation.ValidationResult
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureDatabases() {
|
||||
val database = Database.connect(
|
||||
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
|
||||
user = "root",
|
||||
driver = "org.h2.Driver",
|
||||
password = "",
|
||||
)
|
||||
val userService = UserService(database)
|
||||
routing {
|
||||
// Create user
|
||||
post("/users") {
|
||||
val user = call.receive<ExposedUser>()
|
||||
val id = userService.create(user)
|
||||
call.respond(HttpStatusCode.Created, id)
|
||||
}
|
||||
|
||||
// Read user
|
||||
get("/users/{id}") {
|
||||
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
|
||||
val user = userService.read(id)
|
||||
if (user != null) {
|
||||
call.respond(HttpStatusCode.OK, user)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user
|
||||
put("/users/{id}") {
|
||||
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
|
||||
val user = call.receive<ExposedUser>()
|
||||
userService.update(id, user)
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
// Delete user
|
||||
delete("/users/{id}") {
|
||||
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
|
||||
userService.delete(id)
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
34
backend/src/main/kotlin/HTTP.kt
Normal file
34
backend/src/main/kotlin/HTTP.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.compression.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.requestvalidation.RequestValidation
|
||||
import io.ktor.server.plugins.requestvalidation.ValidationResult
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureHTTP() {
|
||||
install(CORS) {
|
||||
allowMethod(HttpMethod.Options)
|
||||
allowMethod(HttpMethod.Put)
|
||||
allowMethod(HttpMethod.Delete)
|
||||
allowMethod(HttpMethod.Patch)
|
||||
allowHeader(HttpHeaders.Authorization)
|
||||
allowHeader("MyCustomHeader")
|
||||
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
|
||||
}
|
||||
install(Compression)
|
||||
}
|
28
backend/src/main/kotlin/Monitoring.kt
Normal file
28
backend/src/main/kotlin/Monitoring.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.compression.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.requestvalidation.RequestValidation
|
||||
import io.ktor.server.plugins.requestvalidation.ValidationResult
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureMonitoring() {
|
||||
install(CallLogging) {
|
||||
level = Level.INFO
|
||||
filter { call -> call.request.path().startsWith("/") }
|
||||
}
|
||||
}
|
41
backend/src/main/kotlin/Routing.kt
Normal file
41
backend/src/main/kotlin/Routing.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.compression.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.requestvalidation.RequestValidation
|
||||
import io.ktor.server.plugins.requestvalidation.ValidationResult
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureRouting() {
|
||||
install(RequestValidation) {
|
||||
validate<String> { bodyText ->
|
||||
if (!bodyText.startsWith("Hello"))
|
||||
ValidationResult.Invalid("Body text should start with 'Hello'")
|
||||
else ValidationResult.Valid
|
||||
}
|
||||
}
|
||||
install(StatusPages) {
|
||||
exception<Throwable> { call, cause ->
|
||||
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
|
||||
}
|
||||
}
|
||||
routing {
|
||||
get("/") {
|
||||
call.respondText("Hello World!")
|
||||
}
|
||||
}
|
||||
}
|
58
backend/src/main/kotlin/Security.kt
Normal file
58
backend/src/main/kotlin/Security.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.compression.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.requestvalidation.RequestValidation
|
||||
import io.ktor.server.plugins.requestvalidation.ValidationResult
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureSecurity() {
|
||||
authentication {
|
||||
basic(name = "myauth1") {
|
||||
realm = "Ktor Server"
|
||||
validate { credentials ->
|
||||
if (credentials.name == credentials.password) {
|
||||
UserIdPrincipal(credentials.name)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form(name = "myauth2") {
|
||||
userParamName = "user"
|
||||
passwordParamName = "password"
|
||||
challenge {
|
||||
/**/
|
||||
}
|
||||
}
|
||||
}
|
||||
routing {
|
||||
authenticate("myauth1") {
|
||||
get("/protected/route/basic") {
|
||||
val principal = call.principal<UserIdPrincipal>()!!
|
||||
call.respondText("Hello ${principal.name}")
|
||||
}
|
||||
}
|
||||
authenticate("myauth2") {
|
||||
get("/protected/route/form") {
|
||||
val principal = call.principal<UserIdPrincipal>()!!
|
||||
call.respondText("Hello ${principal.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
backend/src/main/kotlin/Serialization.kt
Normal file
29
backend/src/main/kotlin/Serialization.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.*
|
||||
import io.github.flaxoos.ktor.server.plugins.ratelimiter.implementations.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.compression.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.requestvalidation.RequestValidation
|
||||
import io.ktor.server.plugins.requestvalidation.ValidationResult
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureSerialization() {
|
||||
routing {
|
||||
get("/json/kotlinx-serialization") {
|
||||
call.respond(mapOf("hello" to "world"))
|
||||
}
|
||||
}
|
||||
}
|
62
backend/src/main/kotlin/UsersSchema.kt
Normal file
62
backend/src/main/kotlin/UsersSchema.kt
Normal file
@@ -0,0 +1,62 @@
|
||||
package svitan.dev
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
@Serializable
|
||||
data class ExposedUser(val name: String, val age: Int)
|
||||
|
||||
class UserService(database: Database) {
|
||||
object Users : Table() {
|
||||
val id = integer("id").autoIncrement()
|
||||
val name = varchar("name", length = 50)
|
||||
val age = integer("age")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
|
||||
init {
|
||||
transaction(database) {
|
||||
SchemaUtils.create(Users)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun create(user: ExposedUser): Int = dbQuery {
|
||||
Users.insert {
|
||||
it[name] = user.name
|
||||
it[age] = user.age
|
||||
}[Users.id]
|
||||
}
|
||||
|
||||
suspend fun read(id: Int): ExposedUser? {
|
||||
return dbQuery {
|
||||
Users.selectAll()
|
||||
.where { Users.id eq id }
|
||||
.map { ExposedUser(it[Users.name], it[Users.age]) }
|
||||
.singleOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun update(id: Int, user: ExposedUser) {
|
||||
dbQuery {
|
||||
Users.update({ Users.id eq id }) {
|
||||
it[name] = user.name
|
||||
it[age] = user.age
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(id: Int) {
|
||||
dbQuery {
|
||||
Users.deleteWhere { Users.id.eq(id) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> dbQuery(block: suspend () -> T): T =
|
||||
newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||
}
|
||||
|
12
backend/src/main/resources/logback.xml
Normal file
12
backend/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
<logger name="io.netty" level="INFO"/>
|
||||
</configuration>
|
21
backend/src/test/kotlin/ApplicationTest.kt
Normal file
21
backend/src/test/kotlin/ApplicationTest.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package svitan.dev
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.testing.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ApplicationTest {
|
||||
|
||||
@Test
|
||||
fun testRoot() = testApplication {
|
||||
application {
|
||||
module()
|
||||
}
|
||||
client.get("/").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user