From bd9c485b6d56447a58edba82bf08466ccc1a3c0b Mon Sep 17 00:00:00 2001 From: Rui Brito Date: Mon, 20 May 2019 19:03:57 +0200 Subject: [PATCH 1/4] imprv: jackson exceptions: JsonParseException and InvalidDefinitionException --- .../kotlin/io/moia/router/RequestHandler.kt | 26 +++++++++++--- .../io/moia/router/RequestHandlerTest.kt | 35 ++++++++++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/router/src/main/kotlin/io/moia/router/RequestHandler.kt b/router/src/main/kotlin/io/moia/router/RequestHandler.kt index 2c8fcce6..74ac118a 100644 --- a/router/src/main/kotlin/io/moia/router/RequestHandler.kt +++ b/router/src/main/kotlin/io/moia/router/RequestHandler.kt @@ -4,6 +4,8 @@ import com.amazonaws.services.lambda.runtime.Context import com.amazonaws.services.lambda.runtime.RequestHandler import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.exc.InvalidFormatException import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -122,6 +124,8 @@ abstract class RequestHandler : RequestHandler): Any = errors + private fun createUnprocessableEntityErrorBody(error: UnprocessableEntityError): Any = createUnprocessableEntityErrorBody(listOf(error)) + open fun createApiExceptionErrorResponse(contentType: MediaType, input: APIGatewayProxyRequestEvent, ex: ApiException): APIGatewayProxyResponseEvent = createErrorBody(ex.toApiError()).let { APIGatewayProxyResponseEvent() @@ -138,19 +142,33 @@ abstract class RequestHandler : RequestHandler createResponse(contentType, input, + ResponseEntity(422, createUnprocessableEntityErrorBody( + UnprocessableEntityError( + message = "INVALID_ENTITY", + code = "ENTITY", + path = "", + details = mapOf("payload" to ex.requestPayloadAsString.orEmpty()))))) + is InvalidDefinitionException -> createResponse(contentType, input, + ResponseEntity(422, createUnprocessableEntityErrorBody( + UnprocessableEntityError( + message = "INVALID_FIELD_FORMAT", + code = "FIELD", + path = ex.path.last().fieldName.orEmpty(), + details = mapOf("cause" to ex.cause?.message.orEmpty()))))) is InvalidFormatException -> createResponse(contentType, input, - ResponseEntity(422, createUnprocessableEntityErrorBody(listOf( + ResponseEntity(422, createUnprocessableEntityErrorBody( UnprocessableEntityError( message = "INVALID_FIELD_FORMAT", code = "FIELD", - path = ex.path.last().fieldName.orEmpty()))))) + path = ex.path.last().fieldName.orEmpty())))) is MissingKotlinParameterException -> createResponse(contentType, input, - ResponseEntity(422, createUnprocessableEntityErrorBody(listOf(UnprocessableEntityError( + ResponseEntity(422, createUnprocessableEntityErrorBody(UnprocessableEntityError( message = "MISSING_REQUIRED_FIELDS", code = "FIELD", - path = ex.parameter.name.orEmpty()))))) + path = ex.parameter.name.orEmpty())))) else -> createResponse(contentType, input, ResponseEntity(500, createErrorBody(ApiError(ex.message.orEmpty(), "INTERNAL_SERVER_ERROR")))) } diff --git a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt index e5f63c93..75bdce32 100644 --- a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt @@ -12,6 +12,7 @@ import io.mockk.mockk import io.moia.router.Router.Companion.router import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import java.time.LocalDate class RequestHandlerTest { @@ -199,6 +200,38 @@ class RequestHandlerTest { assert(response.statusCode).isEqualTo(422) } + @Test + fun `should handle deserialization error, when field can not be parsed to class`() { + + val response = testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/json" + ) + ) + .withBody("""{"greeting": "hello","age": 1, "bday": "2000-01-AA"}"""), mockk() + ) + assert(response.statusCode).isEqualTo(422) + } + + @Test + fun `should handle deserialization error, when json can not be parsed`() { + + val response = testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/json" + ) + ) + .withBody("""{"greeting": "hello", bday: "2000-01-01"}"""), mockk() + ) + assert(response.statusCode).isEqualTo(422) + } + @Test fun `should handle api exception`() { @@ -458,7 +491,7 @@ class RequestHandlerTest { class TestRequestHandler : RequestHandler() { data class TestResponse(val greeting: String) - data class TestRequest(val greeting: String, val age: Int = 0) + data class TestRequest(val greeting: String, val age: Int = 0, val bday: LocalDate = LocalDate.now()) override val router = router { GET("/some") { _: Request -> From 4b072fed38003842584eac12651ba31c9bc26575 Mon Sep 17 00:00:00 2001 From: Rui Brito Date: Mon, 20 May 2019 19:12:50 +0200 Subject: [PATCH 2/4] imprv: increase coverage --- .../src/main/kotlin/io/moia/router/RequestHandler.kt | 10 ++++++++-- .../test/kotlin/io/moia/router/RequestHandlerTest.kt | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/router/src/main/kotlin/io/moia/router/RequestHandler.kt b/router/src/main/kotlin/io/moia/router/RequestHandler.kt index 74ac118a..99f7b138 100644 --- a/router/src/main/kotlin/io/moia/router/RequestHandler.kt +++ b/router/src/main/kotlin/io/moia/router/RequestHandler.kt @@ -148,14 +148,20 @@ abstract class RequestHandler : RequestHandler createResponse(contentType, input, ResponseEntity(422, createUnprocessableEntityErrorBody( UnprocessableEntityError( message = "INVALID_FIELD_FORMAT", code = "FIELD", path = ex.path.last().fieldName.orEmpty(), - details = mapOf("cause" to ex.cause?.message.orEmpty()))))) + details = mapOf( + "cause" to ex.cause?.message.orEmpty(), + "message" to ex.message.orEmpty() + ))))) is InvalidFormatException -> createResponse(contentType, input, ResponseEntity(422, createUnprocessableEntityErrorBody( diff --git a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt index 75bdce32..2966394b 100644 --- a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt @@ -6,6 +6,7 @@ import assertk.assertions.isFalse import assertk.assertions.isNotNull import assertk.assertions.isNullOrEmpty import assertk.assertions.isTrue +import assertk.assertions.startsWith import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent import com.google.common.net.MediaType import io.mockk.mockk @@ -198,6 +199,7 @@ class RequestHandlerTest { .withBody("""{"greeting": "hello","age": "a"}"""), mockk() ) assert(response.statusCode).isEqualTo(422) + assert(response.body).isEqualTo("""[{"message":"INVALID_FIELD_FORMAT","code":"FIELD","path":"age","details":{}}]""") } @Test @@ -214,6 +216,7 @@ class RequestHandlerTest { .withBody("""{"greeting": "hello","age": 1, "bday": "2000-01-AA"}"""), mockk() ) assert(response.statusCode).isEqualTo(422) + assert(response.body).isEqualTo("""[{"message":"INVALID_FIELD_FORMAT","code":"FIELD","path":"bday","details":{"cause":""}}]""") } @Test @@ -230,6 +233,7 @@ class RequestHandlerTest { .withBody("""{"greeting": "hello", bday: "2000-01-01"}"""), mockk() ) assert(response.statusCode).isEqualTo(422) + assert(response.body).isEqualTo("""[{"message":"INVALID_ENTITY","code":"ENTITY","path":"","details":{"payload":"","message":"Unexpected character ('b' (code 98)): was expecting double-quote to start field name\n at [Source: (String)\"{\"greeting\": \"hello\", bday: \"2000-01-01\"}\"; line: 1, column: 24]"}}]""") } @Test From b2cd508109da3895892d20fa3b3c353b56baa5c4 Mon Sep 17 00:00:00 2001 From: Rui Brito Date: Mon, 20 May 2019 19:42:28 +0200 Subject: [PATCH 3/4] imprv: increase coverage --- .../io/moia/router/RequestHandlerTest.kt | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt index 2966394b..f719ad54 100644 --- a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt @@ -6,8 +6,8 @@ import assertk.assertions.isFalse import assertk.assertions.isNotNull import assertk.assertions.isNullOrEmpty import assertk.assertions.isTrue -import assertk.assertions.startsWith import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent +import com.fasterxml.jackson.module.kotlin.readValue import com.google.common.net.MediaType import io.mockk.mockk import io.moia.router.Router.Companion.router @@ -18,6 +18,7 @@ import java.time.LocalDate class RequestHandlerTest { private val testRequestHandler = TestRequestHandler() + private val mapper = testRequestHandler.objectMapper @Test fun `should match request`() { @@ -199,7 +200,12 @@ class RequestHandlerTest { .withBody("""{"greeting": "hello","age": "a"}"""), mockk() ) assert(response.statusCode).isEqualTo(422) - assert(response.body).isEqualTo("""[{"message":"INVALID_FIELD_FORMAT","code":"FIELD","path":"age","details":{}}]""") + val body = mapper.readValue>(response.body) + assert(body.size).isEqualTo(1) + assert(body[0].code).isEqualTo("FIELD") + assert(body[0].message).isEqualTo("INVALID_FIELD_FORMAT") + assert(body[0].path).isEqualTo("age") + assert(body[0].details.isNotEmpty()).isEqualTo(false) } @Test @@ -216,7 +222,12 @@ class RequestHandlerTest { .withBody("""{"greeting": "hello","age": 1, "bday": "2000-01-AA"}"""), mockk() ) assert(response.statusCode).isEqualTo(422) - assert(response.body).isEqualTo("""[{"message":"INVALID_FIELD_FORMAT","code":"FIELD","path":"bday","details":{"cause":""}}]""") + val body = mapper.readValue>(response.body) + assert(body.size).isEqualTo(1) + assert(body[0].code).isEqualTo("FIELD") + assert(body[0].message).isEqualTo("INVALID_FIELD_FORMAT") + assert(body[0].path).isEqualTo("bday") + assert(body[0].details.isNotEmpty()).isEqualTo(true) } @Test @@ -233,7 +244,12 @@ class RequestHandlerTest { .withBody("""{"greeting": "hello", bday: "2000-01-01"}"""), mockk() ) assert(response.statusCode).isEqualTo(422) - assert(response.body).isEqualTo("""[{"message":"INVALID_ENTITY","code":"ENTITY","path":"","details":{"payload":"","message":"Unexpected character ('b' (code 98)): was expecting double-quote to start field name\n at [Source: (String)\"{\"greeting\": \"hello\", bday: \"2000-01-01\"}\"; line: 1, column: 24]"}}]""") + val body = mapper.readValue>(response.body) + assert(body.size).isEqualTo(1) + assert(body[0].code).isEqualTo("ENTITY") + assert(body[0].message).isEqualTo("INVALID_ENTITY") + assert(body[0].path).isEqualTo("") + assert(body[0].details.isNotEmpty()).isEqualTo(true) } @Test From 74035c07c668794bcd7367311c016a52e6aff2c1 Mon Sep 17 00:00:00 2001 From: Rui Brito Date: Fri, 1 Nov 2019 11:54:40 +0100 Subject: [PATCH 4/4] get JWT cognito username to we can log the API caller --- router/src/main/kotlin/io/moia/router/Router.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/router/src/main/kotlin/io/moia/router/Router.kt b/router/src/main/kotlin/io/moia/router/Router.kt index c35295c9..1bbb84c7 100644 --- a/router/src/main/kotlin/io/moia/router/Router.kt +++ b/router/src/main/kotlin/io/moia/router/Router.kt @@ -83,4 +83,5 @@ data class Request(val apiRequest: APIGatewayProxyRequestEvent, val body: I, fun getPathParameter(name: String): String = pathParameters[name] ?: error("Could not find path parameter '$name") fun getQueryParameter(name: String): String? = queryParameters?.get(name) fun getMultiValueQueryStringParameter(name: String): List? = multiValueQueryStringParameters?.get(name) + fun getJwtCognitoUsername(): String? = (JwtAccessor(this.apiRequest).extractJwtClaims()?.get("cognito:username") as? String) }