How to build nested routes in Ktor? - kotlin

I defined my routes in the separate file:
PostRoutes.kt:
fun Route.getPostsRoute() {
get("/posts") {
call.respondText("Posts")
}
}
// Some other routes
fun Application.postRoutes() {
routing {
getPostsRoute()
// Some other routes
}
}
And I setup these routes in Application.kt as it shown below:
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
fun Application.module(testing: Boolean = false) {
routing { // I want to provide the root endpoint (/api/v1) here
postRoutes()
}
}
How can I setup my root endpoint (/api/v1) in this case?
P.S. I've checked their docs, it says to use nested routes but I can't because I need to call routing in postRoutes() that breaks nested routes.
P.P.S. I am a noobie in Ktor and Kotlin.

You can either wrap the getPostsRoute() with the route("/api/v1") inside the postRoutes method or get rid of the postRoutes method and nest your routes inside the routing {}.
import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
embeddedServer(Netty, port = 5555, host = "0.0.0.0") {
postRoutes()
}.start(wait = false)
}
fun Route.getPostsRoute() {
get("/posts") {
call.respondText("Posts")
}
}
fun Application.postRoutes() {
routing {
route("/api/v1") {
getPostsRoute()
}
}
}

Related

Using async func in Compose Web

I should use Ktor client in Compose Web. But, It can't be called in Compose Web due to async/non-async problem.
Environment is template project made by IntelliJ IDEA.
First, I use this:
val client=HttpClient(Js){
install(ContentNegotiation){
json()
}
}
suspend fun shorterCall(url:String):String{
val response = client.get(SERVER_NAME) {
contentType(ContentType.Application.Json)
parameter("url", url)
}
return response.bodyAsText()
}
suspend fun main() {
var i by mutableStateOf("")
renderComposable(rootElementId = "root") {
Div({ style {
height(100.vh)
margin(0.px)
width(100.vw)
} }) {
Input(type = InputType.Url) {
onInput {
val input=it.value.trim()
if(input.startsWith("http"))
i=shorterCall(input)
else
i="NaN"
}
}
Text(i)
}
}
}
Then, I got that error:
Suspend function can be called only within a coroutine body.
So, I tried another one:
import kotlinx.coroutines.*
fun shorterCall(url:String):String{
var ret:String
suspend fun t():String {
val response = client.get(SERVER_NAME) {
contentType(ContentType.Application.Json)
parameter("url", url)
}
return response.bodyAsText()
}
runBlocking{
ret=t()
}
return ret
}
//main func is same as upper one.
Then, I got that error:
Unresolved reference: runBlocking
+editing body 1: When I use GlobalScope.launch or js("JSCode"), It raise that error:
e: Could not find "kotlin" in [/home/user/.local/share/kotlin/daemon]
e: java.lang.IllegalStateException: FATAL ERROR: Could not find "kotlin" in [/home/user/.local/share/kotlin/daemon]
(a lot of internal errors bellow)
You can use the GlobalScope.launch() method to launch a job for a request in a browser environment:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import io.ktor.client.*
import io.ktor.client.engine.js.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
fun main() {
val client = HttpClient(Js) {}
var i by mutableStateOf("")
renderComposable(rootElementId = "root") {
Div({
style {
height(100.vh)
margin(0.px)
width(100.vw)
}
}) {
Input(type = InputType.Url) {
onInput {
val input = it.value.trim()
if (input.startsWith("http")) {
GlobalScope.launch {
i = client.shorterCall(input)
}
} else {
i = "NaN"
}
}
}
Text(i)
}
}
}
suspend fun HttpClient.shorterCall(url: String): String {
val response = get(url) {
contentType(ContentType.Application.Json)
}
return response.bodyAsText()
}

TransactionalEventListener not invoked in #MicronautTest

Problem description
While system end-to-end tests are invoking methods annotated with #TransactionalEventListener, I'm not able to invoke the same methods in narrower tests annotated with #MicronautTest.
I've tested numerous variants with both injected EntityManager and SessionFactory. #MicronautTest(transactional = false) is also tested. Calling JPA-method inside TestSvcWithTxMethod#someMethod is also tested with same result. I've also tried tests without mocking TestAppEventListener.
The below test/code yields
Verification failed: call 1 of 1:
TestAppEventListener(#1).beforeCommit(any())) was not called.
java.lang.AssertionError: Verification failed: call 1 of 1:
TestAppEventListener(#1).beforeCommit(any())) was not called.
Calls to same mock: 1) TestAppEventListener(#1).hashCode()
Environment: Micronaut 3.7.5, Micronaut Data 3.9.3
Minimal reproducible code
Test is failing as well with transactional = false
import io.kotest.core.spec.style.BehaviorSpec
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.kotest5.MicronautKotest5Extension.getMock
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.DefaultTestAppEventListener
import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.TestAppEventListener
import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.TestSvcWrapper
#MicronautTest
class AppEventWithBeforeCommitListenerMockTest(
testSvcWrapper: TestSvcWrapper,
testAppEventListener: TestAppEventListener
) : BehaviorSpec({
given("context with app event listener") {
`when`("calling someMethod") {
val mockBeforeCommitTestListener = getMock(testAppEventListener)
every { mockBeforeCommitTestListener.beforeCommit(any()) } answers {}
every { mockBeforeCommitTestListener.afterRollback(any()) } answers {}
testSvcWrapper.someMethod(message = "call #1")
verify { mockBeforeCommitTestListener.beforeCommit(any()) }
}
}
}) {
#MockBean(DefaultTestAppEventListener::class)
fun mockTestAppEventListener(): TestAppEventListener = mockk()
}
TestSvcWrapper
import jakarta.inject.Singleton
#Singleton
class TestSvcWrapper(
private val testSvcWithTxMethod: TestSvcWithTxMethod
) {
fun someMethod(message: String) {
testSvcWithTxMethod.someMethod(message)
}
}
TestSvcWithTxMethod
import io.micronaut.context.event.ApplicationEventPublisher
import jakarta.inject.Singleton
import javax.transaction.Transactional
#Singleton
open class TestSvcWithTxMethod(
private val eventPublisher: ApplicationEventPublisher<TestEvent>
) {
#Transactional(Transactional.TxType.REQUIRES_NEW)
open fun someMethod(message: String) {
eventPublisher.publishEvent(TestEvent(message))
}
}
TestEvent
import io.micronaut.core.annotation.Introspected
#Introspected
data class TestEvent(val message: String)
TestAppEventListener
interface TestAppEventListener {
fun beforeCommit(event: TestEvent)
fun afterRollback(event: TestEvent)
}
DefaultTestAppEventListener
import io.micronaut.transaction.annotation.TransactionalEventListener
import jakarta.inject.Singleton
import java.util.concurrent.atomic.AtomicInteger
#Singleton
open class DefaultTestAppEventListener : TestAppEventListener {
val receiveCount = AtomicInteger()
#TransactionalEventListener(TransactionalEventListener.TransactionPhase.BEFORE_COMMIT)
override fun beforeCommit(event: TestEvent) {
receiveCount.getAndIncrement()
}
#TransactionalEventListener(TransactionalEventListener.TransactionPhase.AFTER_ROLLBACK)
override fun afterRollback(event: TestEvent) {
receiveCount.getAndIncrement()
}
}
The answer was found in the micronaut-test repo. Key is to inject SynchronousTransactionManager<Any>, create and then commit/rollback transaction.
I was not able to make mock-test from question pass, most likely because of the annotations, but the following tests are working. I made some modifications to the types in question, hence I added code for the new implementations below.
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
import io.micronaut.transaction.SynchronousTransactionManager
import io.micronaut.transaction.support.DefaultTransactionDefinition
import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.TestAppEventListener
import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.TestSvcWithTxMethod
#MicronautTest(transactional = false)
class AppEventWithBeforeCommitListenerTest(
testSvcWithTxMethod: TestSvcWithTxMethod,
testAppEventListener: TestAppEventListener,
transactionManager: SynchronousTransactionManager<Any>
) : BehaviorSpec({
given("context with app event listener") {
`when`("calling someMethod with commit") {
val tx = transactionManager.getTransaction(DefaultTransactionDefinition())
testSvcWithTxMethod.someMethod(message = "call #1")
transactionManager.commit(tx)
then("TestAppEventListener should have received message") {
testAppEventListener.beforeCommitReceiveCount.get() shouldBe 1
}
}
`when`("calling someMethod with rollback") {
val tx = transactionManager.getTransaction(DefaultTransactionDefinition())
testSvcWithTxMethod.someMethod(message = "call #2")
transactionManager.rollback(tx)
then("TestAppEventListener should have received message") {
testAppEventListener.afterRollbackReceiveCount.get() shouldBe 1
}
}
}
})
TestSvcWithTxMethod
import io.micronaut.context.event.ApplicationEventPublisher
import jakarta.inject.Singleton
import javax.transaction.Transactional
#Singleton
open class TestSvcWithTxMethod(
private val eventPublisher: ApplicationEventPublisher<TestEvent>
) {
#Transactional
open fun someMethod(message: String) {
eventPublisher.publishEvent(TestEvent(message))
}
}
TestAppEventListener
import io.micronaut.transaction.annotation.TransactionalEventListener
import jakarta.inject.Singleton
import java.util.concurrent.atomic.AtomicInteger
#Singleton
open class TestAppEventListener {
val beforeCommitReceiveCount = AtomicInteger()
val afterRollbackReceiveCount = AtomicInteger()
#TransactionalEventListener(TransactionalEventListener.TransactionPhase.BEFORE_COMMIT)
open fun beforeCommit(event: TestEvent) {
beforeCommitReceiveCount.getAndIncrement()
}
#TransactionalEventListener(TransactionalEventListener.TransactionPhase.AFTER_ROLLBACK)
open fun afterRollback(event: TestEvent) {
afterRollbackReceiveCount.getAndIncrement()
}
}
TestEvent (unchanged)
import io.micronaut.core.annotation.Introspected
#Introspected
data class TestEvent(val message: String)

How to restrict route access in ktor framework?

How to restrict route access in ktor framework?
//only admin
post("/add") {
call.respondText { "add" }
}
post("/delete") {
call.respondText { "delete" }
}
You can write a method that creates a route that restricts access for admins only. Inside that method, the newly created route is intercepted to inject the code for validation. In the following example, if the header admin has the value 1 then a request is made from an admin otherwise for the /add and /delete routes the response with the 401 Unauthorized status will be returned.
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.util.pipeline.*
fun main() {
embeddedServer(Netty, port = 5555, host = "0.0.0.0") {
routing {
admin {
post("/add") {
call.respondText { "add" }
}
post("/delete") {
call.respondText { "delete" }
}
}
post("/show") {
call.respondText { "show" }
}
}
}.start(wait = false)
}
private val validationPhase = PipelinePhase("Validate")
fun Route.admin(build: Route.() -> Unit): Route {
val route = createChild(AdminSelector())
route.insertPhaseAfter(ApplicationCallPipeline.Features, Authentication.ChallengePhase)
route.insertPhaseAfter(Authentication.ChallengePhase, validationPhase)
route.intercept(validationPhase) {
if (!isAdmin(call.request)) {
call.respond(HttpStatusCode.Forbidden)
finish()
}
}
route.build()
return route
}
class AdminSelector: RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int) = RouteSelectorEvaluation.Transparent
}
fun isAdmin(request: ApplicationRequest): Boolean {
return request.headers["admin"] == "1"
}
For Ktor 2.x the solution proposed by Alexsei does not work anymore because ChallengePhase is now marked as internal as they completely restructured the plugin system.
This code snippet seems to be working for me.
fun Route.authorization(build: Route.() -> Unit): Route {
val route = createChild(CustomSelector())
val plugin = createRouteScopedPlugin("CustomAuthorization") {
on(AuthenticationChecked) { call ->
val principal = call.authentication.principal
// custom logic
}
}
route.install(plugin)
route.build()
return route
}
private class CustomSelector : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int) = RouteSelectorEvaluation.Transparent
}
Of course, you can add parameters to the function specifying the restriction like required roles.
To secure a route...
fun Route.myRoute() = route("test") {
authorization {
get { ... }
}
}
For more details: https://ktor.io/docs/custom-plugins.html

Ktor: Mock Principal

I have following route:
get("/user") {
val principal: UserIdPrincipal = call.principal()
?: return#get call.respond(HttpStatusCode.Unauthorized)
val user = userService.findUserForId(principal.name.toLong())
?: return#get call.respond(HttpStatusCode.Unauthorized)
val userResponse = UserResponse(username = user.username)
call.respond(userResponse)
}
I don't want to test if authentication works for every single of my routes, so I would like to mock call.principal(). Since it is an inline extension function, it cannot be easily mocked. Any ideas how to solve this problem?
If you can change the configuration for the Authentication plugin in a test then you can register a provider, intercept its pipeline in the AuthenticationPipeline.RequestAuthentication phase, to get an access to authentication context, and finally assign new principal. Here is an example:
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
val server = embeddedServer(Netty, port = 8080) {
install(Authentication) {
provider {
pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
context.principal(UserIdPrincipal("principal"))
}
}
}
routing {
authenticate {
get("/user") {
val principal: UserIdPrincipal = call.principal()!!
call.respond(principal.name)
}
}
}
}
server.start(wait = true)
}

How can I make sure all Kotlin coroutines created by a ktor websocket client are cleared up?

I'm trying to wrap my head around Kotlin coroutines and Ktors websocket support. My understanding is that runBlocking will create a scope and that it will block as long as there are coroutines living inside that scope (or child scopes), but I when the call to runBlocking in the test below returns there are still two coroutines alive..
Why am I leaking coroutines here?
package dummy
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets
import io.ktor.client.features.websocket.wss
import io.ktor.http.HttpMethod
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readBytes
import io.ktor.http.cio.websocket.readText
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.*
import kotlinx.coroutines.debug.DebugProbes
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
#ExperimentalCoroutinesApi
#KtorExperimentalAPI
class WebsocketTest {
#Test
fun tidy() {
DebugProbes.install()
runBlocking {
val socketJob = Job()
launch(CoroutineName("Websocket") + socketJob) {
println("Connecting to websocket")
connectWebsocket(socketJob)
println("Websocket dead?")
}
launch(CoroutineName("Ninja socket killer")) {
delay(3500)
println("Killing websocket client")
socketJob.cancel(message = "Time to die..")
}
}
println("\n\n-------")
DebugProbes.dumpCoroutines(System.err)
Assertions.assertEquals(0, DebugProbes.dumpCoroutinesInfo().size, "It would be nice if all coroutines had been cleared up by now..")
}
}
#KtorExperimentalAPI
private suspend fun connectWebsocket(socketJob: CompletableJob) {
val client = HttpClient {
install(WebSockets)
}
socketJob.invokeOnCompletion {
println("Shutting down ktor http client")
client.close()
}
client.wss(
method = HttpMethod.Get,
host = "echo.websocket.org",
port = 443,
path = "/"
) {
send(Frame.Text("Hello World"))
for (frame in incoming) {
when (frame) {
is Frame.Text -> println(frame.readText())
is Frame.Binary -> println(frame.readBytes())
}
delay(1000)
send(Frame.Text("Hello World"))
}
}
}
build.gradle.kts
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
plugins {
kotlin("jvm") version "1.3.41" apply true
}
repositories {
mavenCentral()
}
val ktorVersion = "1.2.3"
val junitVersion = "5.5.1"
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("io.ktor:ktor-client-websockets:$ktorVersion")
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.0-RC2")
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
}
tasks.withType<Test> {
useJUnitPlatform()
testLogging {
showExceptions = true
showStackTraces = true
exceptionFormat = TestExceptionFormat.FULL
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
}
Seems like I have figured it out (obviously just after ripping my hair out long enough to make this post in the first place). When I wrote the post I leaked two coroutines and one of them "solved itself" (I'm not very happy about that, but what ever I do I can't reproduce it).
The second coroutine leaked because Nonce.kt from Ktor explicitly launches a coroutine in GlobalScope.
https://github.com/ktorio/ktor/blob/master/ktor-utils/jvm/src/io/ktor/util/Nonce.kt#L30
private val nonceGeneratorJob =
GlobalScope.launch(
context = Dispatchers.IO + NonCancellable + NonceGeneratorCoroutineName,
start = CoroutineStart.LAZY
) { ....