HI, I want to implement test where I can actually ...
# ktor
v
HI, I want to implement test where I can actually confirm user. My process is user signups he receives an email with link(which includes token) from sendgrid(HtmlEmail from commons.mail) then client make a post request to other my api to confirm. So request to /api/signup will result in request sent to sendgrid. I want to intercept that request in my tests get the body from it and do not send real request to Sendgrid. But send another request with my token to my endpoint. How I can achieve that?
đź‘€ 1
a
Can you fake the dependency that sends a request to Sendgrid in a test?
v
thank you for fast answer, I am not sure how to do that if I have next code in test. Dependency which is making request to sendgrid is
import org.apache.commons.mail.HtmlEmail
Copy code
// this thing will make request to sendgrid internally
    <http://client.post|client.post>("/api/v1/auth/signup") {
        headers.remove("Authorization")
        contentType(ContentType.Application.Json)
        setBody(body)
    }

    <http://client.post|client.post>("/api/v1/auth/confirm-email") {
        headers.remove("Authorization")
        contentType(ContentType.Application.Json)
        setBody(ConfirmEmailDTO("this should be retrived from call which is made inside of service"))
    }
b
I need to write a similar test case but haven't started writing it yet. Based on my limited understanding I was going to try using the MockEngine for my client, if that is applicable to your use case. https://github.com/ktorio/ktor-documentation/blob/main/codeSnippets/snippets/client-testing-mock/src/test/kotlin/ApplicationTest.kt
a
@Viktor Orlyk can you explain what behavior you need to test?
v
@BryanT yes I was thinking about MockEngine, but haven’t found how to use it to actually do that. @Aleksei Tirman [JB] I want to write end to end test which will go through full sign up proccess: 1. sign up(post to my server) 2. email with token( done in server) 3. confirmation request (post to my server) I was thinking about wiremock, but haven’t found how to extract the request from it and reuse data from it. My other thoughts are next code.
Copy code
class AuthSpec : FreeSpec({
  val ds = createDataSourceTest()
  "check login functionality" - {
    "login confirmed user" - {
      val signupData = SignupDTO("<mailto:giloja3553@andorem.com|giloja3553@andorem.com>", "<mailto:Giloja3553@andorem.com|Giloja3553@andorem.com>")
      testApplication {
        val client  = createApplicationAndGetClient(ds)
        <http://client.post|client.post>("/api/v1/auth/signup") {
          headers.remove("Authorization")
          contentType(ContentType.Application.Json)
          setBody(body)
        }
// somehow  I need to intercept previout request to sendgrid and retrieve token and use it in request below
        <http://client.post|client.post>("/api/v1/auth/confirm-email") {
          headers.remove("Authorization")
          contentType(ContentType.Application.Json)
          setBody(ConfirmEmailDTO("hello"))
        }
        val loginResponse = <http://client.post|client.post>("/api/v1/auth/login"){
          headers.remove("Authorization")
          contentType(ContentType.Application.Json)
          setBody(LoginDTO(signupData.email, signupData.password))
        }
        val body = loginResponse.body<LoginResponseDTO>()
        body.access.shouldNotBeNull()
        body.refresh.shouldNotBeNull()
      }
    }
  }
})
my initial thought was to create a mock client but it fails for some reason
Copy code
val mockEngine = MockEngine { call ->
        when (call.url.toString()) {
            "<https://api.sendgrid.com/v3/mail/send>" -> {
                println(call.body)
                respond(content = "Ok", status = HttpStatusCode.OK)
            }
            else -> {
                error("Unhandled ${call.url}")
            }
        }
    }
    val mockClient = HttpClient(mockEngine){
        install(ContentNegotiation){
            json()
        }
    }
<http://mockClient.post|mockClient.post>("/api/v1/auth/signup").....
but it fails with next error
Copy code
Unhandled <http://localhost/api/v1/auth/signup>
java.lang.IllegalStateException: Unhandled <http://localhost/api/v1/auth/signup>
	at com.coinypal.features.auth.AuthSpec$1$1$1$1$mockEngine$1.invokeSuspend(AuthSpec.kt:29)
	at com.coinypal.features.auth.AuthSpec$1$1$1$1$mockEngine$1.invoke(AuthSpec.kt)
	at com.coinypal.features.auth.AuthSpec$1$1$1$1$mockEngine$1.invoke(AuthSpec.kt)
	at io.ktor.client.engine.mock.MockEngine.execute(MockEngine.kt:71)
	at io.ktor.client.engine.HttpClientEngine$executeWithinCallContext$2.invokeSuspend(HttpClientEngine.kt:99)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
if I use this mockClient instead of client which was created and returned in the first code block
Copy code
fun ApplicationTestBuilder.createApplicationAndGetClient(ds:DataSource): HttpClient {
  application{
    DbFactory.init(ds)

    configureExceptions()
    configureDependencyInjection()
    configureMustache()
    configureCors()
    configureSerialization()
    configureAuthentication()
    configureRouting()
  }

  return createClient {
    install(ContentNegotiation){
      json()
    }
  }
}
a
You can pass a fake version of the service, that sends requests to SendGrid, inside the
application
block of
testApplication
by manually calling the module function.
v
@Aleksei Tirman [JB] so there is no way to intercept that request currently? because I did that as a simple function which is just called, not module which is injected via some di. Or maybe i don’t understand how i can replace that function which is nested somewhere in the service layer with something new. Anyway thank you for answers!
I moved quite far with my solution but probably I am missing something important in MockEngine work, for some reason it does not sotp in debugger in inside of MockEngine
Copy code
"login confirmed user" - {
      val signupData = SignupDTO("<mailto:giloja3553@andorem.com|giloja3553@andorem.com>", "<mailto:Giloja3553@andorem.com|Giloja3553@andorem.com>")
      testApplication {
        var token: String = "this should be changed"
        val mockClient = createClient {
          engine {
            MockEngine { call ->
              when (call.url.toString()) {
                "<https://api.sendgrid.com/v3/mail/send>" -> {
                  println("hello ${call.body}")
                  token = call.body.toString() // here I will find the token
                  respond(content = "Ok", status = HttpStatusCode.OK)
                }
                else -> {
                  error("Unhandled ${call.url}")
                }
              }
            }
          }
          install(ContentNegotiation){
            json()
          }

        }
        val client  = createApplicationAndGetClient(ds)
        <http://mockClient.post|mockClient.post>("/api/v1/auth/signup") {
          headers.remove("Authorization")
          contentType(ContentType.Application.Json)
          setBody(body)
        }

        <http://client.post|client.post>("/api/v1/auth/confirm-email") {
          headers.remove("Authorization")
          contentType(ContentType.Application.Json)
          setBody(ConfirmEmailDTO(token))
        }
        val loginResponse = <http://client.post|client.post>("/api/v1/auth/login"){
          headers.remove("Authorization")
          contentType(ContentType.Application.Json)
          setBody(LoginDTO(signupData.email, signupData.password))
        }
        val body = loginResponse.body<LoginResponseDTO>()
        body.access.shouldNotBeNull()
        body.refresh.shouldNotBeNull()
      }
    }
a
That’s because you create an instance of the
MockEngine
inside the
engine
’s block but don’t assign it. You can’t change the engine of the client inside the
testApplication
.