Skip to content

Commit

Permalink
feat: rest echo can handle non json request
Browse files Browse the repository at this point in the history
  • Loading branch information
jgiovaresco committed Jan 22, 2023
1 parent 295223b commit d87a191
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 36 deletions.
78 changes: 66 additions & 12 deletions app/src/main/kotlin/io/apim/samples/rest/EchoHandler.kt
@@ -1,28 +1,82 @@
package io.apim.samples.rest

import io.vertx.core.http.HttpHeaders
import io.vertx.core.json.DecodeException
import io.vertx.core.json.JsonObject
import io.vertx.ext.web.impl.ParsableMIMEValue
import io.vertx.kotlin.core.json.json
import io.vertx.kotlin.core.json.obj
import io.vertx.rxjava3.core.MultiMap
import io.vertx.rxjava3.ext.web.ParsedHeaderValues
import io.vertx.rxjava3.ext.web.RequestBody
import io.vertx.rxjava3.ext.web.RoutingContext

fun echoHandler(ctx: RoutingContext) {
ctx.response()
.setStatusCode(200)
.putHeader("Content-Type", "application/json")
.end(
json {
var body: JsonObject
val response = ctx.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")

try {
body = json {
obj(
"method" to ctx.request().method().name(),
"headers" to obj(ctx.request().headers().toSimpleMap()),
"query_params" to obj(ctx.request().params().toSimpleMap()),
"body" to handleBody(ctx.body(), ctx.parsedHeaders())
)
}
response.statusCode = 200
} catch (e: DecodeException) {
response.statusCode = 400
body = json {
obj(
"title" to "The request body fail to be parsed",
"detail" to e.cause?.message
)
}
}

response.end(body.toString()).subscribe()
}

fun handleBody(body: RequestBody, headers: ParsedHeaderValues): JsonObject {
val contentType = (headers.delegate.contentType() as ParsableMIMEValue).forceParse()

if (contentType.isText()) {
return json {
obj(
"method" to ctx.request().method().name(),
"headers" to obj(ctx.request().headers().toSimpleMap()),
"query_params" to obj(ctx.request().params().toSimpleMap()),
"body" to ctx.body().asJsonObject()
"type" to "text",
"content" to body.asString()
)
}.toString()
}
}

if (contentType.isJson()) {
return json {
obj(
"type" to "json",
"content" to body.asJsonObject()
)
}
}

return json {
obj(
"type" to "unknown",
"content" to body.asString()
)
.subscribe()
}
}

/** Transform a multimap into a simple map. Multiple values are joined in a string separated with ; */
/** Transform a MultiMap into a simple map. Multiple values are joined in a string separated with ; */
fun MultiMap.toSimpleMap() = this.entries()
.groupBy { it.key }
.mapValues { it.value.joinToString(";") { h -> h.value } }

fun ParsableMIMEValue.isText(): Boolean {
return this.component() == "text"
}

fun ParsableMIMEValue.isJson(): Boolean {
return this.component() == "application" && this.subComponent().contains("json")
}
147 changes: 123 additions & 24 deletions app/src/test/kotlin/io/apim/samples/rest/RestServerVerticleTest.kt
@@ -1,13 +1,15 @@
package io.apim.samples.rest

import io.reactivex.rxjava3.kotlin.subscribeBy
import io.vertx.core.http.HttpHeaders
import io.vertx.ext.web.client.WebClientOptions
import io.vertx.junit5.VertxExtension
import io.vertx.junit5.VertxTestContext
import io.vertx.kotlin.core.json.json
import io.vertx.kotlin.core.json.obj
import io.vertx.rxjava3.config.ConfigRetriever
import io.vertx.rxjava3.core.Vertx
import io.vertx.rxjava3.core.buffer.Buffer
import io.vertx.rxjava3.ext.web.client.WebClient
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
Expand All @@ -21,6 +23,7 @@ import strikt.api.expectThat
import strikt.assertions.contains
import strikt.assertions.containsExactly
import strikt.assertions.isEqualTo
import strikt.assertions.isNull
import kotlin.io.path.Path

@ExtendWith(VertxExtension::class)
Expand Down Expand Up @@ -90,37 +93,133 @@ class RestServerVerticleTest {
}
}

@Test
fun `should return POST request in response body`() {
val body = json {
obj(
"message" to "hello!",
"attribute" to "value"
)
@Nested
inner class PostRequest {
@ParameterizedTest
@ValueSource(
strings = [
"application/json",
"application/vnd.company.api-v1+json",
]
)
fun `should return json request in response body`(contentType: String) {
val body = json {
obj(
"message" to "hello!",
"attribute" to "value"
)
}

client.post("/echo")
.putHeader(HttpHeaders.CONTENT_TYPE.toString(), contentType)
.sendJsonObject(body)
.test()
.await()
.assertNoErrors()
.assertValue { result ->
expectThat(result.bodyAsJsonObject()) {
get { getString("method") }.isEqualTo("POST")

and {
get { getJsonObject("headers").getString("user-agent") }.contains("Vert.x-WebClient")
get { getJsonObject("headers").getString("host") }.isEqualTo("localhost:8888")
get { getJsonObject("headers").getString("content-type") }.isEqualTo(contentType)
get { getJsonObject("headers").getString("content-length") }.isEqualTo(body.toString().length.toString())
}

and {
get { getJsonObject("body").getString("type") }.isEqualTo("json")
get { getJsonObject("body").getJsonObject("content") }.isEqualTo(body)
}
}
true
}
}

client.post("/echo")
.sendJsonObject(body)
.test()
.await()
.assertNoErrors()
.assertValue { result ->
expectThat(result.bodyAsJsonObject()) {
get { getString("method") }.isEqualTo("POST")
@ParameterizedTest
@ValueSource(
strings = [
"text/plain",
"text/html",
"text/xml"
]
)
fun `should return text request in response body`(contentType: String) {
val body = "a random text"

and {
get { getJsonObject("headers").getString("user-agent") }.contains("Vert.x-WebClient")
get { getJsonObject("headers").getString("host") }.isEqualTo("localhost:8888")
get { getJsonObject("headers").getString("content-type") }.isEqualTo("application/json")
get { getJsonObject("headers").getString("content-length") }.isEqualTo(body.toString().length.toString())
client.post("/echo")
.putHeader(HttpHeaders.CONTENT_TYPE.toString(), contentType)
.sendBuffer(Buffer.buffer(body))
.test()
.await()
.assertNoErrors()
.assertValue { result ->
expectThat(result.bodyAsJsonObject()) {
get { getString("method") }.isEqualTo("POST")

and {
get { getJsonObject("headers").getString("user-agent") }.contains("Vert.x-WebClient")
get { getJsonObject("headers").getString("host") }.isEqualTo("localhost:8888")
get { getJsonObject("headers").getString("content-type") }.isEqualTo(contentType)
get { getJsonObject("headers").getString("content-length") }.isEqualTo(body.length.toString())
}

and {
get { getJsonObject("body").getString("type") }.isEqualTo("text")
get { getJsonObject("body").getString("content") }.isEqualTo(body)
}
}
true
}
}

and {
get { getJsonObject("body") }.isEqualTo(body)
@Test
fun `should return unknown type body request in response body`() {
val body = "unknown"

client.post("/echo")
.sendBuffer(Buffer.buffer(body))
.test()
.await()
.assertNoErrors()
.assertValue { result ->
expectThat(result.bodyAsJsonObject()) {
get { getString("method") }.isEqualTo("POST")

and {
get { getJsonObject("headers").getString("user-agent") }.contains("Vert.x-WebClient")
get { getJsonObject("headers").getString("host") }.isEqualTo("localhost:8888")
get { getJsonObject("headers").getString("content-type") }.isNull()
get { getJsonObject("headers").getString("content-length") }.isEqualTo(body.length.toString())
}

and {
get { getJsonObject("body").getString("type") }.isEqualTo("unknown")
get { getJsonObject("body").getString("content") }.isEqualTo(body)
}
}
true
}
true
}
}

@Test
fun `should return a bad request error when malformed Json request`() {
val body = "a message"

client.post("/echo")
.putHeader(HttpHeaders.CONTENT_TYPE.toString(), "application/json")
.sendBuffer(Buffer.buffer(body))
.test()
.await()
.assertNoErrors()
.assertValue { result ->
expectThat(result.bodyAsJsonObject()) {
get { getString("title") }.isEqualTo("The request body fail to be parsed")
get { getString("detail") }.contains("Unrecognized token 'a': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')")
}
true
}
}
}
}

Expand Down

0 comments on commit d87a191

Please sign in to comment.