From e7d5668501747e423f1617fc091a22d6f6820aaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:37:24 +0200 Subject: [PATCH] Bump org.jmailen.kotlinter from 3.12.0 to 4.3.0 (#333) * Bump org.jmailen.kotlinter from 3.12.0 to 4.3.0 Bumps org.jmailen.kotlinter from 3.12.0 to 4.3.0. --- updated-dependencies: - dependency-name: org.jmailen.kotlinter dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Format Kotlin * Format Kotlin * Fix linting * Fix linting * Fix linting * Fix linting * Fix linting * Fix linting * Fix linting --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yannick Block --- build.gradle.kts | 2 +- .../moia/router/openapi/OpenApiValidator.kt | 65 +- .../openapi/ValidatingRequestRouterWrapper.kt | 29 +- .../router/openapi/OpenApiValidatorTest.kt | 47 +- .../ValidatingRequestRouterWrapperTest.kt | 37 +- .../io/moia/router/proto/ProtoBufUtils.kt | 4 +- .../proto/ProtoDeserializationHandler.kt | 5 +- .../proto/ProtoEnabledRequestHandler.kt | 9 +- .../router/proto/ProtoSerializationHandler.kt | 12 +- .../io/moia/router/proto/ProtoBufUtilsTest.kt | 22 +- .../proto/ProtoDeserializationHandlerTest.kt | 11 +- .../moia/router/proto/RequestHandlerTest.kt | 139 +-- .../router/APIGatewayProxyEventExtensions.kt | 90 +- .../kotlin/io/moia/router/ApiException.kt | 13 +- .../io/moia/router/DeserializationHandler.kt | 34 +- .../io/moia/router/MediaTypeExtensions.kt | 5 +- .../io/moia/router/PermissionHandler.kt | 17 +- .../kotlin/io/moia/router/RequestHandler.kt | 244 +++-- .../kotlin/io/moia/router/RequestPredicate.kt | 17 +- .../kotlin/io/moia/router/ResponseEntity.kt | 54 +- .../src/main/kotlin/io/moia/router/Router.kt | 68 +- .../io/moia/router/SerializationHandler.kt | 49 +- .../main/kotlin/io/moia/router/UriTemplate.kt | 29 +- .../APIGatewayProxyEventExtensionsTest.kt | 38 +- .../kotlin/io/moia/router/ApiRequestTest.kt | 1 - .../router/JsonDeserializationHandlerTest.kt | 33 +- .../moia/router/JwtPermissionHandlerTest.kt | 26 +- .../kotlin/io/moia/router/MediaTypeTest.kt | 1 - .../moia/router/NoOpPermissionHandlerTest.kt | 1 - .../PlainTextDeserializationHandlerTest.kt | 17 +- .../PlainTextSerializationHandlerTest.kt | 1 - .../io/moia/router/RequestHandlerTest.kt | 953 +++++++++--------- .../io/moia/router/ResponseEntityTest.kt | 8 +- .../test/kotlin/io/moia/router/RouterTest.kt | 100 +- .../kotlin/io/moia/router/UriTemplateTest.kt | 114 ++- 35 files changed, 1274 insertions(+), 1021 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 74e06212..7e523af7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,7 @@ plugins { `maven-publish` jacoco id("com.github.kt3k.coveralls") version "2.12.2" - id("org.jmailen.kotlinter") version "3.12.0" + id("org.jmailen.kotlinter") version "4.3.0" } group = "com.github.moia-dev" diff --git a/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/OpenApiValidator.kt b/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/OpenApiValidator.kt index b8005d35..9e6ce1fc 100644 --- a/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/OpenApiValidator.kt +++ b/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/OpenApiValidator.kt @@ -13,19 +13,25 @@ import org.slf4j.LoggerFactory class OpenApiValidator(val specUrlOrPayload: String) { val validator = OpenApiInteractionValidator.createFor(specUrlOrPayload).build() - fun validate(request: APIGatewayProxyRequestEvent, response: APIGatewayProxyResponseEvent): ValidationReport { + fun validate( + request: APIGatewayProxyRequestEvent, + response: APIGatewayProxyResponseEvent, + ): ValidationReport { return validator.validate(request.toRequest(), response.toResponse()) .also { if (it.hasErrors()) log.error("error validating request and response against $specUrlOrPayload - $it") } } - fun assertValid(request: APIGatewayProxyRequestEvent, response: APIGatewayProxyResponseEvent) { + fun assertValid( + request: APIGatewayProxyRequestEvent, + response: APIGatewayProxyResponseEvent, + ) { return validate(request, response).let { if (it.hasErrors()) { throw ApiInteractionInvalid( specUrlOrPayload, request, response, - it + it, ) } } @@ -37,38 +43,45 @@ class OpenApiValidator(val specUrlOrPayload: String) { throw ApiInteractionInvalid( spec = specUrlOrPayload, request = request, - validationReport = it + validationReport = it, ) } } - fun assertValidResponse(request: APIGatewayProxyRequestEvent, response: APIGatewayProxyResponseEvent) = - request.toRequest().let { r -> - validator.validateResponse(r.path, r.method, response.toResponse()).let { - if (it.hasErrors()) { - throw ApiInteractionInvalid( - spec = specUrlOrPayload, - request = request, - validationReport = it - ) - } + fun assertValidResponse( + request: APIGatewayProxyRequestEvent, + response: APIGatewayProxyResponseEvent, + ) = request.toRequest().let { r -> + validator.validateResponse(r.path, r.method, response.toResponse()).let { + if (it.hasErrors()) { + throw ApiInteractionInvalid( + spec = specUrlOrPayload, + request = request, + validationReport = it, + ) } } + } - class ApiInteractionInvalid(val spec: String, val request: APIGatewayProxyRequestEvent, val response: APIGatewayProxyResponseEvent? = null, val validationReport: ValidationReport) : - RuntimeException("Error validating request and response against $spec - $validationReport") + class ApiInteractionInvalid( + val spec: String, + val request: APIGatewayProxyRequestEvent, + val response: APIGatewayProxyResponseEvent? = null, + val validationReport: ValidationReport, + ) : RuntimeException("Error validating request and response against $spec - $validationReport") private fun APIGatewayProxyRequestEvent.toRequest(): Request { - val builder = when (httpMethod.toLowerCase()) { - "get" -> SimpleRequest.Builder.get(path) - "post" -> SimpleRequest.Builder.post(path) - "put" -> SimpleRequest.Builder.put(path) - "patch" -> SimpleRequest.Builder.patch(path) - "delete" -> SimpleRequest.Builder.delete(path) - "options" -> SimpleRequest.Builder.options(path) - "head" -> SimpleRequest.Builder.head(path) - else -> throw IllegalArgumentException("Unsupported method $httpMethod") - } + val builder = + when (httpMethod.toLowerCase()) { + "get" -> SimpleRequest.Builder.get(path) + "post" -> SimpleRequest.Builder.post(path) + "put" -> SimpleRequest.Builder.put(path) + "patch" -> SimpleRequest.Builder.patch(path) + "delete" -> SimpleRequest.Builder.delete(path) + "options" -> SimpleRequest.Builder.options(path) + "head" -> SimpleRequest.Builder.head(path) + else -> throw IllegalArgumentException("Unsupported method $httpMethod") + } headers?.forEach { builder.withHeader(it.key, it.value) } queryStringParameters?.forEach { builder.withQueryParam(it.key, it.value) } builder.withBody(body) diff --git a/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapper.kt b/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapper.kt index ba559666..268f1692 100644 --- a/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapper.kt +++ b/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapper.kt @@ -20,17 +20,33 @@ class ValidatingRequestRouterWrapper( val delegate: RequestHandler, specUrlOrPayload: String, private val additionalRequestValidationFunctions: List<(APIGatewayProxyRequestEvent) -> Unit> = emptyList(), - private val additionalResponseValidationFunctions: List<(APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent) -> Unit> = emptyList() + private val additionalResponseValidationFunctions: List< + ( + APIGatewayProxyRequestEvent, + APIGatewayProxyResponseEvent, + ) -> Unit, + > = emptyList(), ) { private val openApiValidator = OpenApiValidator(specUrlOrPayload) - fun handleRequest(input: APIGatewayProxyRequestEvent, context: Context): APIGatewayProxyResponseEvent = + fun handleRequest( + input: APIGatewayProxyRequestEvent, + context: Context, + ): APIGatewayProxyResponseEvent = handleRequest(input = input, context = context, skipRequestValidation = false, skipResponseValidation = false) - fun handleRequestSkippingRequestAndResponseValidation(input: APIGatewayProxyRequestEvent, context: Context): APIGatewayProxyResponseEvent = + fun handleRequestSkippingRequestAndResponseValidation( + input: APIGatewayProxyRequestEvent, + context: Context, + ): APIGatewayProxyResponseEvent = handleRequest(input = input, context = context, skipRequestValidation = true, skipResponseValidation = true) - private fun handleRequest(input: APIGatewayProxyRequestEvent, context: Context, skipRequestValidation: Boolean, skipResponseValidation: Boolean): APIGatewayProxyResponseEvent { + private fun handleRequest( + input: APIGatewayProxyRequestEvent, + context: Context, + skipRequestValidation: Boolean, + skipResponseValidation: Boolean, + ): APIGatewayProxyResponseEvent { if (!skipRequestValidation) { try { openApiValidator.assertValidRequest(input) @@ -58,7 +74,10 @@ class ValidatingRequestRouterWrapper( additionalRequestValidationFunctions.forEach { it(requestEvent) } } - private fun runAdditionalResponseValidations(requestEvent: APIGatewayProxyRequestEvent, responseEvent: APIGatewayProxyResponseEvent) { + private fun runAdditionalResponseValidations( + requestEvent: APIGatewayProxyRequestEvent, + responseEvent: APIGatewayProxyResponseEvent, + ) { additionalResponseValidationFunctions.forEach { it(requestEvent, responseEvent) } } diff --git a/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/OpenApiValidatorTest.kt b/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/OpenApiValidatorTest.kt index 928b6379..da0252fa 100644 --- a/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/OpenApiValidatorTest.kt +++ b/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/OpenApiValidatorTest.kt @@ -10,15 +10,15 @@ import org.assertj.core.api.BDDAssertions.thenThrownBy import org.junit.jupiter.api.Test class OpenApiValidatorTest { - val testHandler = TestRequestHandler() val validator = OpenApiValidator("openapi.yml") @Test fun `should handle and validate request`() { - val request = GET("/tests") - .withHeaders(mapOf("Accept" to "application/json")) + val request = + GET("/tests") + .withHeaders(mapOf("Accept" to "application/json")) val response = testHandler.handleRequest(request, mockk()) @@ -29,8 +29,9 @@ class OpenApiValidatorTest { @Test fun `should fail on undocumented request`() { - val request = GET("/tests-not-documented") - .withHeaders(mapOf("Accept" to "application/json")) + val request = + GET("/tests-not-documented") + .withHeaders(mapOf("Accept" to "application/json")) val response = testHandler.handleRequest(request, mockk()) @@ -40,37 +41,39 @@ class OpenApiValidatorTest { @Test fun `should fail on invalid schema`() { - val request = GET("/tests") - .withHeaders(mapOf("Accept" to "application/json")) + val request = + GET("/tests") + .withHeaders(mapOf("Accept" to "application/json")) - val response = TestInvalidRequestHandler() - .handleRequest(request, mockk()) + val response = + TestInvalidRequestHandler() + .handleRequest(request, mockk()) thenThrownBy { validator.assertValid(request, response) }.isInstanceOf(OpenApiValidator.ApiInteractionInvalid::class.java) } class TestRequestHandler : RequestHandler() { - data class TestResponse(val name: String) - override val router = Router.router { - GET("/tests") { _: Request -> - ResponseEntity.ok(TestResponse("Hello")) + override val router = + Router.router { + GET("/tests") { _: Request -> + ResponseEntity.ok(TestResponse("Hello")) + } + GET("/tests-not-documented") { _: Request -> + ResponseEntity.ok(TestResponse("Hello")) + } } - GET("/tests-not-documented") { _: Request -> - ResponseEntity.ok(TestResponse("Hello")) - } - } } class TestInvalidRequestHandler : RequestHandler() { - data class TestResponseInvalid(val invalid: String) - override val router = Router.router { - GET("/tests") { _: Request -> - ResponseEntity.ok(TestResponseInvalid("Hello")) + override val router = + Router.router { + GET("/tests") { _: Request -> + ResponseEntity.ok(TestResponseInvalid("Hello")) + } } - } } } diff --git a/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapperTest.kt b/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapperTest.kt index 09ea08f1..db8a752a 100644 --- a/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapperTest.kt +++ b/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapperTest.kt @@ -12,11 +12,11 @@ import org.assertj.core.api.BDDAssertions.thenThrownBy import org.junit.jupiter.api.Test class ValidatingRequestRouterWrapperTest { - @Test fun `should return response on successful validation`() { - val response = ValidatingRequestRouterWrapper(TestRequestHandler(), "openapi.yml") - .handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) + val response = + ValidatingRequestRouterWrapper(TestRequestHandler(), "openapi.yml") + .handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) then(response.statusCode).isEqualTo(200) } @@ -43,8 +43,12 @@ class ValidatingRequestRouterWrapperTest { @Test fun `should skip validation`() { - val response = ValidatingRequestRouterWrapper(InvalidTestRequestHandler(), "openapi.yml") - .handleRequestSkippingRequestAndResponseValidation(GET("/path-not-documented").withAcceptHeader("application/json"), mockk()) + val response = + ValidatingRequestRouterWrapper(InvalidTestRequestHandler(), "openapi.yml") + .handleRequestSkippingRequestAndResponseValidation( + GET("/path-not-documented").withAcceptHeader("application/json"), + mockk(), + ) then(response.statusCode).isEqualTo(404) } @@ -54,7 +58,7 @@ class ValidatingRequestRouterWrapperTest { ValidatingRequestRouterWrapper( delegate = OpenApiValidatorTest.TestRequestHandler(), specUrlOrPayload = "openapi.yml", - additionalRequestValidationFunctions = listOf({ _ -> throw RequestValidationFailedException() }) + additionalRequestValidationFunctions = listOf({ _ -> throw RequestValidationFailedException() }), ) .handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) } @@ -67,7 +71,7 @@ class ValidatingRequestRouterWrapperTest { ValidatingRequestRouterWrapper( delegate = OpenApiValidatorTest.TestRequestHandler(), specUrlOrPayload = "openapi.yml", - additionalResponseValidationFunctions = listOf({ _, _ -> throw ResponseValidationFailedException() }) + additionalResponseValidationFunctions = listOf({ _, _ -> throw ResponseValidationFailedException() }), ) .handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) } @@ -75,21 +79,24 @@ class ValidatingRequestRouterWrapperTest { } private class RequestValidationFailedException : RuntimeException("request validation failed") + private class ResponseValidationFailedException : RuntimeException("request validation failed") private class TestRequestHandler : RequestHandler() { - override val router = router { - GET("/tests") { _: Request -> - ResponseEntity.ok("""{"name": "some"}""") + override val router = + router { + GET("/tests") { _: Request -> + ResponseEntity.ok("""{"name": "some"}""") + } } - } } private class InvalidTestRequestHandler : RequestHandler() { - override val router = router { - GET("/tests") { _: Request -> - ResponseEntity.notFound(Unit) + override val router = + router { + GET("/tests") { _: Request -> + ResponseEntity.notFound(Unit) + } } - } } } diff --git a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoBufUtils.kt b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoBufUtils.kt index db1882a7..e07ecf63 100644 --- a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoBufUtils.kt +++ b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoBufUtils.kt @@ -15,7 +15,7 @@ object ProtoBufUtils { fun removeWrapperObjects(json: String): String { return removeWrapperObjects( - jacksonObjectMapper().readTree(json) + jacksonObjectMapper().readTree(json), ).toString() } @@ -38,7 +38,7 @@ object ProtoBufUtils { if (entry.value.size() > 0) { result.replace( entry.key, - removeWrapperObjects(entry.value) + removeWrapperObjects(entry.value), ) } } else { diff --git a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoDeserializationHandler.kt b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoDeserializationHandler.kt index db88cfc8..36a163aa 100644 --- a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoDeserializationHandler.kt +++ b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoDeserializationHandler.kt @@ -22,7 +22,10 @@ class ProtoDeserializationHandler : DeserializationHandler { MediaType.parse(input.contentType()).let { proto.isCompatibleWith(it) || protoStructuredSuffixWildcard.isCompatibleWith(it) } } - override fun deserialize(input: APIGatewayProxyRequestEvent, target: KType?): Any { + override fun deserialize( + input: APIGatewayProxyRequestEvent, + target: KType?, + ): Any { val bytes = Base64.getDecoder().decode(input.body) val parser = (target?.classifier as KClass<*>).staticFunctions.first { it.name == "parser" }.call() as Parser<*> return parser.parseFrom(bytes) diff --git a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoEnabledRequestHandler.kt b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoEnabledRequestHandler.kt index c6d3e01a..56c38342 100644 --- a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoEnabledRequestHandler.kt +++ b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoEnabledRequestHandler.kt @@ -6,16 +6,13 @@ import io.moia.router.RequestHandler import io.moia.router.ResponseEntity abstract class ProtoEnabledRequestHandler : RequestHandler() { + override fun serializationHandlers() = listOf(ProtoSerializationHandler()) + super.serializationHandlers() - override fun serializationHandlers() = - listOf(ProtoSerializationHandler()) + super.serializationHandlers() - - override fun deserializationHandlers() = - listOf(ProtoDeserializationHandler()) + super.deserializationHandlers() + override fun deserializationHandlers() = listOf(ProtoDeserializationHandler()) + super.deserializationHandlers() override fun createResponse( contentType: MediaType, - response: ResponseEntity + response: ResponseEntity, ): APIGatewayProxyResponseEvent { return super.createResponse(contentType, response).withIsBase64Encoded(true) } diff --git a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoSerializationHandler.kt b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoSerializationHandler.kt index e4b150a6..e6fd932c 100644 --- a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoSerializationHandler.kt +++ b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoSerializationHandler.kt @@ -7,14 +7,18 @@ import isCompatibleWith import java.util.Base64 class ProtoSerializationHandler : SerializationHandler { - private val json = MediaType.parse("application/json") private val jsonStructuredSuffixWildcard = MediaType.parse("application/*+json") - override fun supports(acceptHeader: MediaType, body: Any): Boolean = - body is GeneratedMessageV3 + override fun supports( + acceptHeader: MediaType, + body: Any, + ): Boolean = body is GeneratedMessageV3 - override fun serialize(acceptHeader: MediaType, body: Any): String { + override fun serialize( + acceptHeader: MediaType, + body: Any, + ): String { val message = body as GeneratedMessageV3 return if (json.isCompatibleWith(acceptHeader) || jsonStructuredSuffixWildcard.isCompatibleWith(acceptHeader)) { ProtoBufUtils.toJsonWithoutWrappers(message) diff --git a/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoBufUtilsTest.kt b/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoBufUtilsTest.kt index 21906d34..4b60734f 100644 --- a/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoBufUtilsTest.kt +++ b/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoBufUtilsTest.kt @@ -8,12 +8,12 @@ import org.assertj.core.api.BDDAssertions.then import org.junit.jupiter.api.Test class ProtoBufUtilsTest { - @Test fun `should serialize empty list`() { - val message = ComplexSample.newBuilder() - .addAllSamples(emptyList()) - .build() + val message = + ComplexSample.newBuilder() + .addAllSamples(emptyList()) + .build() val json = ProtoBufUtils.toJsonWithoutWrappers(message) @@ -22,9 +22,10 @@ class ProtoBufUtilsTest { @Test fun `should remove wrapper object`() { - val message = ComplexSample.newBuilder() - .setSomeString(StringValue.newBuilder().setValue("some").build()) - .build() + val message = + ComplexSample.newBuilder() + .setSomeString(StringValue.newBuilder().setValue("some").build()) + .build() val json = ProtoBufUtils.toJsonWithoutWrappers(message) @@ -33,9 +34,10 @@ class ProtoBufUtilsTest { @Test fun `should serialize value when it is the default`() { - val message = ComplexSample.newBuilder() - .setEnumAttribute(ONE) // enum zero value - .build() + val message = + ComplexSample.newBuilder() + .setEnumAttribute(ONE) // enum zero value + .build() val json = ProtoBufUtils.toJsonWithoutWrappers(message) diff --git a/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoDeserializationHandlerTest.kt b/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoDeserializationHandlerTest.kt index 10ea53de..bbbec384 100644 --- a/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoDeserializationHandlerTest.kt +++ b/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoDeserializationHandlerTest.kt @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test internal class ProtoDeserializationHandlerTest { - @Test fun `Deserializer should not support if the content type of the input is null`() { assertFalse(ProtoDeserializationHandler().supports(APIGatewayProxyRequestEvent())) @@ -20,7 +19,13 @@ internal class ProtoDeserializationHandlerTest { @Test fun `Deserializer should support if the content type of the input is protobuf`() { - assertTrue(ProtoDeserializationHandler().supports(APIGatewayProxyRequestEvent().withHeader("content-type", "application/x-protobuf"))) - assertTrue(ProtoDeserializationHandler().supports(APIGatewayProxyRequestEvent().withHeader("content-type", "application/vnd.moia.v1+x-protobuf"))) + assertTrue( + ProtoDeserializationHandler().supports(APIGatewayProxyRequestEvent().withHeader("content-type", "application/x-protobuf")), + ) + assertTrue( + ProtoDeserializationHandler().supports( + APIGatewayProxyRequestEvent().withHeader("content-type", "application/vnd.moia.v1+x-protobuf"), + ), + ) } } diff --git a/router-protobuf/src/test/kotlin/io/moia/router/proto/RequestHandlerTest.kt b/router-protobuf/src/test/kotlin/io/moia/router/proto/RequestHandlerTest.kt index 3b43ca7b..96d5c362 100644 --- a/router-protobuf/src/test/kotlin/io/moia/router/proto/RequestHandlerTest.kt +++ b/router-protobuf/src/test/kotlin/io/moia/router/proto/RequestHandlerTest.kt @@ -18,18 +18,18 @@ import org.junit.jupiter.api.Test import java.util.Base64 class RequestHandlerTest { - private val testRequestHandler = TestRequestHandler() @Test fun `should match request to proto handler and return json`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some-proto") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some-proto") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""{"hello":"Hello","request":""}""") @@ -37,13 +37,14 @@ class RequestHandlerTest { @Test fun `should match request to proto handler with version accept header and return json`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some-proto") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/vnd.moia.v1+json")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some-proto") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/vnd.moia.v1+json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""{"hello":"v1","request":""}""") @@ -51,13 +52,14 @@ class RequestHandlerTest { @Test fun `should match request to proto handler and return proto`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some-proto") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/x-protobuf")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some-proto") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/x-protobuf")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(Sample.parseFrom(response.bodyAsBytes())).isEqualTo(Sample.newBuilder().setHello("Hello").setRequest("").build()) @@ -67,19 +69,20 @@ class RequestHandlerTest { fun `should match request to proto handler and deserialize and return proto`() { val request = Sample.newBuilder().setHello("Hello").setRequest("").build() - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some-proto") - .withHttpMethod("POST") - .withBody(Base64.getEncoder().encodeToString(request.toByteArray())) - .withHeaders( - mapOf( - "Accept" to "application/x-protobuf", - "Content-Type" to "application/x-protobuf" - ) - ), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some-proto") + .withHttpMethod("POST") + .withBody(Base64.getEncoder().encodeToString(request.toByteArray())) + .withHeaders( + mapOf( + "Accept" to "application/x-protobuf", + "Content-Type" to "application/x-protobuf", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(Sample.parseFrom(response.bodyAsBytes())).isEqualTo(request) @@ -88,31 +91,35 @@ class RequestHandlerTest { @Test fun `should return 406-unacceptable error in proto`() { - val response = testRequestHandler.handleRequest( - GET("/some-proto") - .withHeaders( - mapOf( - "Accept" to "text/plain" - ) - ), - mockk() - ) + val response = + testRequestHandler.handleRequest( + GET("/some-proto") + .withHeaders( + mapOf( + "Accept" to "text/plain", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(406) - assertThat(io.moia.router.proto.sample.SampleOuterClass.ApiError.parseFrom(response.bodyAsBytes()).getCode()).isEqualTo("NOT_ACCEPTABLE") + assertThat( + io.moia.router.proto.sample.SampleOuterClass.ApiError.parseFrom(response.bodyAsBytes()).getCode(), + ).isEqualTo("NOT_ACCEPTABLE") } @Test fun `should return api error in protos`() { - val response = testRequestHandler.handleRequest( - GET("/some-error") - .withHeaders( - mapOf( - "Accept" to "application/x-protobuf" - ) - ), - mockk() - ) + val response = + testRequestHandler.handleRequest( + GET("/some-error") + .withHeaders( + mapOf( + "Accept" to "application/x-protobuf", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(400) with(io.moia.router.proto.sample.SampleOuterClass.ApiError.parseFrom(response.bodyAsBytes())) { @@ -122,21 +129,21 @@ class RequestHandlerTest { } class TestRequestHandler : ProtoEnabledRequestHandler() { + override val router = + router { + defaultProducing = setOf("application/x-protobuf") + defaultConsuming = setOf("application/x-protobuf") - override val router = router { - defaultProducing = setOf("application/x-protobuf") - defaultConsuming = setOf("application/x-protobuf") - - defaultContentType = "application/x-protobuf" + defaultContentType = "application/x-protobuf" - GET("/some-proto") { _: Request -> ResponseEntity.ok(Sample.newBuilder().setHello("v1").build()) } - .producing("application/vnd.moia.v1+x-protobuf", "application/vnd.moia.v1+json") + GET("/some-proto") { _: Request -> ResponseEntity.ok(Sample.newBuilder().setHello("v1").build()) } + .producing("application/vnd.moia.v1+x-protobuf", "application/vnd.moia.v1+json") - GET("/some-proto") { _: Request -> ResponseEntity.ok(Sample.newBuilder().setHello("Hello").build()) } - .producing("application/x-protobuf", "application/json") - POST("/some-proto") { r: Request -> ResponseEntity.ok(r.body) } - GET("/some-error") { _: Request -> throw ApiException("boom", "BOOM", 400) } - } + GET("/some-proto") { _: Request -> ResponseEntity.ok(Sample.newBuilder().setHello("Hello").build()) } + .producing("application/x-protobuf", "application/json") + POST("/some-proto") { r: Request -> ResponseEntity.ok(r.body) } + GET("/some-error") { _: Request -> throw ApiException("boom", "BOOM", 400) } + } override fun createErrorBody(error: ApiError): Any = io.moia.router.proto.sample.SampleOuterClass.ApiError.newBuilder() diff --git a/router/src/main/kotlin/io/moia/router/APIGatewayProxyEventExtensions.kt b/router/src/main/kotlin/io/moia/router/APIGatewayProxyEventExtensions.kt index 8df705ad..148d7082 100644 --- a/router/src/main/kotlin/io/moia/router/APIGatewayProxyEventExtensions.kt +++ b/router/src/main/kotlin/io/moia/router/APIGatewayProxyEventExtensions.kt @@ -26,28 +26,48 @@ import java.util.Base64 data class Header(val name: String, val value: String) fun APIGatewayProxyRequestEvent.acceptHeader() = getHeaderCaseInsensitive("accept") -fun APIGatewayProxyRequestEvent.acceptedMediaTypes() = acceptHeader() - ?.split(",") - ?.map { it.trim() } - ?.mapNotNull { parseMediaTypeSafe(it) } - .orEmpty() + +fun APIGatewayProxyRequestEvent.acceptedMediaTypes() = + acceptHeader() + ?.split(",") + ?.map { it.trim() } + ?.mapNotNull { parseMediaTypeSafe(it) } + .orEmpty() + fun APIGatewayProxyRequestEvent.contentType() = getHeaderCaseInsensitive("content-type") -fun APIGatewayProxyRequestEvent.getHeaderCaseInsensitive(httpHeader: String): String? = - getCaseInsensitive(httpHeader, headers) +fun APIGatewayProxyRequestEvent.getHeaderCaseInsensitive(httpHeader: String): String? = getCaseInsensitive(httpHeader, headers) -fun APIGatewayProxyResponseEvent.getHeaderCaseInsensitive(httpHeader: String): String? = - getCaseInsensitive(httpHeader, headers) +fun APIGatewayProxyResponseEvent.getHeaderCaseInsensitive(httpHeader: String): String? = getCaseInsensitive(httpHeader, headers) +@Suppress("FunctionName") fun GET() = APIGatewayProxyRequestEvent().withHttpMethod("get").withHeaders(mutableMapOf()) + +@Suppress("FunctionName") fun GET(path: String) = GET().withPath(path) + +@Suppress("FunctionName") fun POST() = APIGatewayProxyRequestEvent().withHttpMethod("post").withHeaders(mutableMapOf()) + +@Suppress("FunctionName") fun POST(path: String) = POST().withPath(path) + +@Suppress("FunctionName") fun PUT() = APIGatewayProxyRequestEvent().withHttpMethod("put").withHeaders(mutableMapOf()) + +@Suppress("FunctionName") fun PUT(path: String) = PUT().withPath(path) + +@Suppress("FunctionName") fun PATCH() = APIGatewayProxyRequestEvent().withHttpMethod("patch").withHeaders(mutableMapOf()) + +@Suppress("FunctionName") fun PATCH(path: String) = PATCH().withPath(path) + +@Suppress("FunctionName") fun DELETE() = APIGatewayProxyRequestEvent().withHttpMethod("delete").withHeaders(mutableMapOf()) + +@Suppress("FunctionName") fun DELETE(path: String) = DELETE().withPath(path) /** @@ -59,41 +79,47 @@ fun DELETE(path: String) = DELETE().withPath(path) fun APIGatewayProxyRequestEvent.location(path: String): URI { val host = getHeaderCaseInsensitive("host") ?: "localhost" val proto = getHeaderCaseInsensitive("x-forwarded-proto") ?: "http" - val portPart = getHeaderCaseInsensitive("x-forwarded-port") - ?.let { - when { - proto == "https" && it == "443" -> null - proto == "http" && it == "80" -> null - else -> ":$it" - } - } ?: "" + val portPart = + getHeaderCaseInsensitive("x-forwarded-port") + ?.let { + when { + proto == "https" && it == "443" -> null + proto == "http" && it == "80" -> null + else -> ":$it" + } + } ?: "" return URI("$proto://$host$portPart/${path.removePrefix("/")}") } -fun APIGatewayProxyRequestEvent.withHeader(name: String, value: String) = - this.also { if (headers == null) headers = mutableMapOf() }.also { headers[name] = value } +fun APIGatewayProxyRequestEvent.withHeader( + name: String, + value: String, +) = this.also { if (headers == null) headers = mutableMapOf() }.also { headers[name] = value } -fun APIGatewayProxyRequestEvent.withHeader(header: Header) = - this.withHeader(header.name, header.value) +fun APIGatewayProxyRequestEvent.withHeader(header: Header) = this.withHeader(header.name, header.value) -fun APIGatewayProxyRequestEvent.withAcceptHeader(accept: String) = - this.withHeader("accept", accept) +fun APIGatewayProxyRequestEvent.withAcceptHeader(accept: String) = this.withHeader("accept", accept) -fun APIGatewayProxyRequestEvent.withContentTypeHeader(contentType: String) = - this.withHeader("content-type", contentType) +fun APIGatewayProxyRequestEvent.withContentTypeHeader(contentType: String) = this.withHeader("content-type", contentType) -fun APIGatewayProxyResponseEvent.withHeader(name: String, value: String) = - this.also { if (headers == null) headers = mutableMapOf() }.also { headers[name] = value } +fun APIGatewayProxyResponseEvent.withHeader( + name: String, + value: String, +) = this.also { if (headers == null) headers = mutableMapOf() }.also { headers[name] = value } -fun APIGatewayProxyResponseEvent.withHeader(header: Header) = - this.withHeader(header.name, header.value) +fun APIGatewayProxyResponseEvent.withHeader(header: Header) = this.withHeader(header.name, header.value) -fun APIGatewayProxyResponseEvent.withLocationHeader(request: APIGatewayProxyRequestEvent, path: String) = - this.also { if (headers == null) headers = mutableMapOf() }.also { headers["location"] = request.location(path).toString() } +fun APIGatewayProxyResponseEvent.withLocationHeader( + request: APIGatewayProxyRequestEvent, + path: String, +) = this.also { if (headers == null) headers = mutableMapOf() }.also { headers["location"] = request.location(path).toString() } fun APIGatewayProxyResponseEvent.location() = getHeaderCaseInsensitive("location") -private fun getCaseInsensitive(key: String, map: Map?): String? = +private fun getCaseInsensitive( + key: String, + map: Map?, +): String? = map?.entries ?.firstOrNull { key.equals(it.key, ignoreCase = true) } ?.value diff --git a/router/src/main/kotlin/io/moia/router/ApiException.kt b/router/src/main/kotlin/io/moia/router/ApiException.kt index 8f825d9b..60f6ecc0 100644 --- a/router/src/main/kotlin/io/moia/router/ApiException.kt +++ b/router/src/main/kotlin/io/moia/router/ApiException.kt @@ -21,29 +21,26 @@ open class ApiException( val code: String, val httpResponseStatus: Int, val details: Map = emptyMap(), - cause: Throwable? = null + cause: Throwable? = null, ) : RuntimeException(message, cause) { - override fun toString(): String { return "ApiException(message='$message', code='$code', httpResponseStatus=$httpResponseStatus, details=$details, cause=$cause)" } - fun toApiError() = - ApiError(super.message!!, code, details) + fun toApiError() = ApiError(super.message!!, code, details) - inline fun toResponseEntity(mapper: (error: ApiError) -> Any = {}) = - ResponseEntity(httpResponseStatus, mapper(toApiError())) + inline fun toResponseEntity(mapper: (error: ApiError) -> Any = {}) = ResponseEntity(httpResponseStatus, mapper(toApiError())) } data class ApiError( val message: String, val code: String, - val details: Map = emptyMap() + val details: Map = emptyMap(), ) data class UnprocessableEntityError( val message: String, val code: String, val path: String, - val details: Map = emptyMap() + val details: Map = emptyMap(), ) diff --git a/router/src/main/kotlin/io/moia/router/DeserializationHandler.kt b/router/src/main/kotlin/io/moia/router/DeserializationHandler.kt index 569ffd15..7a4e12bd 100644 --- a/router/src/main/kotlin/io/moia/router/DeserializationHandler.kt +++ b/router/src/main/kotlin/io/moia/router/DeserializationHandler.kt @@ -26,24 +26,25 @@ import kotlin.reflect.KType import kotlin.reflect.full.isSubclassOf interface DeserializationHandler { - fun supports(input: APIGatewayProxyRequestEvent): Boolean - fun deserialize(input: APIGatewayProxyRequestEvent, target: KType?): Any? + fun deserialize( + input: APIGatewayProxyRequestEvent, + target: KType?, + ): Any? } class DeserializationHandlerChain(private val handlers: List) : DeserializationHandler { + override fun supports(input: APIGatewayProxyRequestEvent): Boolean = handlers.any { it.supports(input) } - override fun supports(input: APIGatewayProxyRequestEvent): Boolean = - handlers.any { it.supports(input) } - - override fun deserialize(input: APIGatewayProxyRequestEvent, target: KType?): Any? = - handlers.firstOrNull { it.supports(input) }?.deserialize(input, target) + override fun deserialize( + input: APIGatewayProxyRequestEvent, + target: KType?, + ): Any? = handlers.firstOrNull { it.supports(input) }?.deserialize(input, target) } class JsonDeserializationHandler(private val objectMapper: ObjectMapper) : DeserializationHandler { - private val json = MediaType.parse("application/json; charset=UTF-8") private val jsonStructuredSuffixWildcard = MediaType.parse("application/*+json; charset=UTF-8") @@ -55,15 +56,19 @@ class JsonDeserializationHandler(private val objectMapper: ObjectMapper) : Deser .let { json.isCompatibleWith(it) || jsonStructuredSuffixWildcard.isCompatibleWith(it) } } - override fun deserialize(input: APIGatewayProxyRequestEvent, target: KType?): Any? { + override fun deserialize( + input: APIGatewayProxyRequestEvent, + target: KType?, + ): Any? { val targetClass = target?.classifier as KClass<*> return when { targetClass == Unit::class -> Unit targetClass == String::class -> input.body!! targetClass.isSubclassOf(Collection::class) -> { val kClass = target.arguments.first().type!!.classifier as KClass<*> - val type = TypeFactory.defaultInstance() - .constructParametricType(targetClass.javaObjectType, kClass.javaObjectType) + val type = + TypeFactory.defaultInstance() + .constructParametricType(targetClass.javaObjectType, kClass.javaObjectType) objectMapper.readValue(input.body, type) } else -> objectMapper.readValue(input.body, targetClass.java) @@ -73,6 +78,7 @@ class JsonDeserializationHandler(private val objectMapper: ObjectMapper) : Deser object PlainTextDeserializationHandler : DeserializationHandler { private val text = MediaType.parse("text/*") + override fun supports(input: APIGatewayProxyRequestEvent): Boolean = if (input.contentType() == null) { false @@ -80,6 +86,8 @@ object PlainTextDeserializationHandler : DeserializationHandler { MediaType.parse(input.contentType()!!).isCompatibleWith(text) } - override fun deserialize(input: APIGatewayProxyRequestEvent, target: KType?): Any? = - input.body + override fun deserialize( + input: APIGatewayProxyRequestEvent, + target: KType?, + ): Any? = input.body } diff --git a/router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt b/router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt index 376a3170..8f538e57 100644 --- a/router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt +++ b/router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt @@ -21,6 +21,7 @@ fun MediaType.isCompatibleWith(other: MediaType): Boolean = true } else { type() == other.type() && (subtype().contains("+") && other.subtype().contains("+")) && this.subtype() - .substringBeforeLast("+") == "*" && this.subtype().substringAfterLast("+") == other.subtype() - .substringAfterLast("+") && (other.parameters().isEmpty || this.parameters() == other.parameters()) + .substringBeforeLast("+") == "*" && this.subtype().substringAfterLast("+") == + other.subtype() + .substringAfterLast("+") && (other.parameters().isEmpty || this.parameters() == other.parameters()) } diff --git a/router/src/main/kotlin/io/moia/router/PermissionHandler.kt b/router/src/main/kotlin/io/moia/router/PermissionHandler.kt index 121aa8e5..202dfc98 100644 --- a/router/src/main/kotlin/io/moia/router/PermissionHandler.kt +++ b/router/src/main/kotlin/io/moia/router/PermissionHandler.kt @@ -35,9 +35,8 @@ class NoOpPermissionHandler : PermissionHandler { open class JwtAccessor( private val request: APIGatewayProxyRequestEvent, - private val authorizationHeaderName: String = "authorization" + private val authorizationHeaderName: String = "authorization", ) { - private val objectMapper = jacksonObjectMapper() fun extractJwtToken(): String? = @@ -58,16 +57,16 @@ open class JwtAccessor( } ?.let { objectMapper.readValue>(it) } } + open class JwtPermissionHandler( val accessor: JwtAccessor, - val permissionsClaim: String = defaultPermissionsClaim, - val permissionSeparator: String = defaultPermissionSeparator + val permissionsClaim: String = DEFAULT_PERMISSIONS_CLAIM, + val permissionSeparator: String = DEFAULT_PERMISSION_SEPARATOR, ) : PermissionHandler { - constructor( request: APIGatewayProxyRequestEvent, - permissionsClaim: String = defaultPermissionsClaim, - permissionSeparator: String = defaultPermissionSeparator + permissionsClaim: String = DEFAULT_PERMISSIONS_CLAIM, + permissionSeparator: String = DEFAULT_PERMISSION_SEPARATOR, ) : this(JwtAccessor(request), permissionsClaim, permissionSeparator) override fun hasAnyRequiredPermission(requiredPermissions: Set): Boolean = @@ -90,7 +89,7 @@ open class JwtPermissionHandler( ?: emptySet() companion object { - private const val defaultPermissionsClaim = "scope" - private const val defaultPermissionSeparator: String = " " + private const val DEFAULT_PERMISSIONS_CLAIM = "scope" + private const val DEFAULT_PERMISSION_SEPARATOR: String = " " } } diff --git a/router/src/main/kotlin/io/moia/router/RequestHandler.kt b/router/src/main/kotlin/io/moia/router/RequestHandler.kt index b731fe97..ff323248 100644 --- a/router/src/main/kotlin/io/moia/router/RequestHandler.kt +++ b/router/src/main/kotlin/io/moia/router/RequestHandler.kt @@ -34,7 +34,6 @@ import kotlin.reflect.jvm.reflect @Suppress("UnstableApiUsage") abstract class RequestHandler : RequestHandler { - open val objectMapper = jacksonObjectMapper() abstract val router: Router @@ -42,7 +41,10 @@ abstract class RequestHandler : RequestHandler> - val matchResults: List = routes.map { routerFunction: RouterFunction -> - val matchResult = routerFunction.requestPredicate.match(input) - log.debug("match result for route '$routerFunction' is '$matchResult'") - if (matchResult.match) { - val matchedAcceptType = routerFunction.requestPredicate.matchedAcceptType(input.acceptedMediaTypes()) - ?: MediaType.parse(router.defaultContentType) + val matchResults: List = + routes.map { routerFunction: RouterFunction -> + val matchResult = routerFunction.requestPredicate.match(input) + log.debug("match result for route '$routerFunction' is '$matchResult'") + if (matchResult.match) { + val matchedAcceptType = + routerFunction.requestPredicate.matchedAcceptType(input.acceptedMediaTypes()) + ?: MediaType.parse(router.defaultContentType) - val handler: HandlerFunction = routerFunction.handler + val handler: HandlerFunction = routerFunction.handler - val response = - try { - if (missingPermissions(input, routerFunction)) { - throw ApiException("missing permissions", "MISSING_PERMISSIONS", 403) - } else { - val requestBody = deserializeRequest(handler, input) - val request = Request(input, requestBody, routerFunction.requestPredicate.pathPattern) - (handler as HandlerFunction<*, *>)(request) + val response = + try { + if (missingPermissions(input, routerFunction)) { + throw ApiException("missing permissions", "MISSING_PERMISSIONS", 403) + } else { + val requestBody = deserializeRequest(handler, input) + val request = Request(input, requestBody, routerFunction.requestPredicate.pathPattern) + (handler as HandlerFunction<*, *>)(request) + } + } catch (e: Exception) { + exceptionToResponseEntity(e, input) } - } catch (e: Exception) { - exceptionToResponseEntity(e, input) - } - return createResponse(matchedAcceptType, response) + return createResponse(matchedAcceptType, response) + } + matchResult } - matchResult - } return handleNonDirectMatch(MediaType.parse(router.defaultContentType), matchResults, input) } - private fun exceptionToResponseEntity(e: Exception, input: APIGatewayProxyRequestEvent) = - when (e) { - is ApiException -> - e.toResponseEntity(this::createErrorBody) - .also { logApiException(e, input) } - else -> - exceptionToResponseEntity(e) - .also { logUnknownException(e, input) } - } + private fun exceptionToResponseEntity( + e: Exception, + input: APIGatewayProxyRequestEvent, + ) = when (e) { + is ApiException -> + e.toResponseEntity(this::createErrorBody) + .also { logApiException(e, input) } + else -> + exceptionToResponseEntity(e) + .also { logUnknownException(e, input) } + } - private fun missingPermissions(input: APIGatewayProxyRequestEvent, routerFunction: RouterFunction): Boolean { + private fun missingPermissions( + input: APIGatewayProxyRequestEvent, + routerFunction: RouterFunction, + ): Boolean { if (predicatePermissionHandlerSupplier() != null) { return !predicatePermissionHandlerSupplier()!!(input).hasAnyRequiredPermission(routerFunction.requestPredicate) } @@ -100,38 +112,48 @@ abstract class RequestHandler : RequestHandler = listOf( - JsonSerializationHandler(objectMapper), - PlainTextSerializationHandler() - ) + open fun serializationHandlers(): List = + listOf( + JsonSerializationHandler(objectMapper), + PlainTextSerializationHandler(), + ) - open fun deserializationHandlers(): List = listOf( - JsonDeserializationHandler(objectMapper) - ) + open fun deserializationHandlers(): List = + listOf( + JsonDeserializationHandler(objectMapper), + ) - open fun permissionHandlerSupplier(): (r: APIGatewayProxyRequestEvent) -> PermissionHandler = - { NoOpPermissionHandler() } + open fun permissionHandlerSupplier(): (r: APIGatewayProxyRequestEvent) -> PermissionHandler = { NoOpPermissionHandler() } open fun predicatePermissionHandlerSupplier(): ((r: APIGatewayProxyRequestEvent) -> PredicatePermissionHandler)? = null @ExperimentalReflectionOnLambdas private fun deserializeRequest( handler: HandlerFunction, - input: APIGatewayProxyRequestEvent + input: APIGatewayProxyRequestEvent, ): Any? { - val requestType = handler.reflect()?.parameters?.first()?.type?.arguments?.first()?.type - ?: throw IllegalArgumentException("reflection failed, try using a real lambda instead of function references (Kotlin 1.6 bug?)") + val requestType = + handler.reflect()?.parameters?.first()?.type?.arguments?.first()?.type + ?: throw IllegalArgumentException( + "reflection failed, try using a real lambda instead of function references (Kotlin 1.6 bug?)", + ) return when { requestType.classifier as KClass<*> == Unit::class -> Unit input.body == null && requestType.isMarkedNullable -> null @@ -141,7 +163,11 @@ abstract class RequestHandler : RequestHandler, input: APIGatewayProxyRequestEvent): APIGatewayProxyResponseEvent { + private fun handleNonDirectMatch( + defaultContentType: MediaType, + matchResults: List, + input: APIGatewayProxyRequestEvent, + ): APIGatewayProxyResponseEvent { // no direct match val apiException = when { @@ -149,29 +175,30 @@ abstract class RequestHandler : RequestHandler ApiException( httpResponseStatus = 406, message = "Not Acceptable", - code = "NOT_ACCEPTABLE" + code = "NOT_ACCEPTABLE", ) matchResults.any { it.matchPath && !it.matchMethod } -> ApiException( httpResponseStatus = 405, message = "Method Not Allowed", - code = "METHOD_NOT_ALLOWED" + code = "METHOD_NOT_ALLOWED", + ) + else -> + ApiException( + httpResponseStatus = 404, + message = "Not found", + code = "NOT_FOUND", ) - else -> ApiException( - httpResponseStatus = 404, - message = "Not found", - code = "NOT_FOUND" - ) } return createResponse( contentType = input.acceptedMediaTypes().firstOrNull() ?: defaultContentType, - response = apiException.toResponseEntity(this::createErrorBody) + response = apiException.toResponseEntity(this::createErrorBody), ) } @@ -194,58 +221,67 @@ abstract class RequestHandler : RequestHandler ResponseEntity( - 422, - createUnprocessableEntityErrorBody( - UnprocessableEntityError( - message = "INVALID_ENTITY", - code = "ENTITY", - path = "", - details = mapOf( - "payload" to ex.requestPayloadAsString.orEmpty(), - "message" to ex.message.orEmpty() - ) - ) + is JsonParseException -> + ResponseEntity( + 422, + createUnprocessableEntityErrorBody( + UnprocessableEntityError( + message = "INVALID_ENTITY", + code = "ENTITY", + path = "", + details = + mapOf( + "payload" to ex.requestPayloadAsString.orEmpty(), + "message" to ex.message.orEmpty(), + ), + ), + ), ) - ) - is InvalidDefinitionException -> ResponseEntity( - 422, - createUnprocessableEntityErrorBody( - UnprocessableEntityError( - message = "INVALID_FIELD_FORMAT", - code = "FIELD", - path = ex.path.last().fieldName.orEmpty(), - details = mapOf( - "cause" to ex.cause?.message.orEmpty(), - "message" to ex.message.orEmpty() - ) - ) + is InvalidDefinitionException -> + ResponseEntity( + 422, + createUnprocessableEntityErrorBody( + UnprocessableEntityError( + message = "INVALID_FIELD_FORMAT", + code = "FIELD", + path = ex.path.last().fieldName.orEmpty(), + details = + mapOf( + "cause" to ex.cause?.message.orEmpty(), + "message" to ex.message.orEmpty(), + ), + ), + ), ) - ) - is InvalidFormatException -> ResponseEntity( - 422, - createUnprocessableEntityErrorBody( - UnprocessableEntityError( - message = "INVALID_FIELD_FORMAT", - code = "FIELD", - path = ex.path.last().fieldName.orEmpty() - ) + is InvalidFormatException -> + ResponseEntity( + 422, + createUnprocessableEntityErrorBody( + UnprocessableEntityError( + message = "INVALID_FIELD_FORMAT", + code = "FIELD", + path = ex.path.last().fieldName.orEmpty(), + ), + ), ) - ) - is MissingKotlinParameterException -> ResponseEntity( - 422, - createUnprocessableEntityErrorBody( - UnprocessableEntityError( - message = "MISSING_REQUIRED_FIELDS", - code = "FIELD", - path = ex.parameter.name.orEmpty() - ) + is MissingKotlinParameterException -> + ResponseEntity( + 422, + createUnprocessableEntityErrorBody( + UnprocessableEntityError( + message = "MISSING_REQUIRED_FIELDS", + code = "FIELD", + path = ex.parameter.name.orEmpty(), + ), + ), ) - ) else -> ResponseEntity(500, createErrorBody(ApiError(ex.message.orEmpty(), "INTERNAL_SERVER_ERROR"))) } - open fun createResponse(contentType: MediaType, response: ResponseEntity): APIGatewayProxyResponseEvent = + open fun createResponse( + contentType: MediaType, + response: ResponseEntity, + ): APIGatewayProxyResponseEvent = when (response.body != null && serializationHandlerChain.supports(contentType, response.body)) { true -> contentType false -> MediaType.parse(router.defaultContentType) @@ -256,7 +292,7 @@ abstract class RequestHandler : RequestHandler): MediaType? val pathPattern: String @@ -42,10 +44,10 @@ open class RequestPredicateImpl( override val method: String, override val pathPattern: String, override var produces: Set, - override var consumes: Set + override var consumes: Set, ) : RequestPredicate { - override var requiredPermissions: Set = emptySet() + override fun consuming(vararg mediaTypes: String): RequestPredicate { consumes = mediaTypes.toSet() return this @@ -70,11 +72,11 @@ open class RequestPredicateImpl( matchPath = pathMatches(request), matchMethod = methodMatches(request), matchAcceptType = acceptMatches(request.acceptedMediaTypes()), - matchContentType = contentTypeMatches(request.contentType()) + matchContentType = contentTypeMatches(request.contentType()), ) - private fun pathMatches(request: APIGatewayProxyRequestEvent) = - request.path?.let { UriTemplate.from(pathPattern).matches(it) } ?: false + private fun pathMatches(request: APIGatewayProxyRequestEvent) = request.path?.let { UriTemplate.from(pathPattern).matches(it) } ?: false + private fun methodMatches(request: APIGatewayProxyRequestEvent) = method.equals(request.httpMethod, true) /** @@ -86,8 +88,7 @@ open class RequestPredicateImpl( .map { MediaType.parse(it) } .firstOrNull { acceptedMediaTypes.any { acceptedType -> it.isCompatibleWith(acceptedType) } } - private fun acceptMatches(acceptedMediaTypes: List) = - matchedAcceptType(acceptedMediaTypes) != null + private fun acceptMatches(acceptedMediaTypes: List) = matchedAcceptType(acceptedMediaTypes) != null private fun contentTypeMatches(contentType: String?) = when { @@ -101,7 +102,7 @@ data class RequestMatchResult( val matchPath: Boolean = false, val matchMethod: Boolean = false, val matchAcceptType: Boolean = false, - val matchContentType: Boolean = false + val matchContentType: Boolean = false, ) { val match get() = matchPath && matchMethod && matchAcceptType && matchContentType diff --git a/router/src/main/kotlin/io/moia/router/ResponseEntity.kt b/router/src/main/kotlin/io/moia/router/ResponseEntity.kt index 3219a8fb..6285fa8c 100644 --- a/router/src/main/kotlin/io/moia/router/ResponseEntity.kt +++ b/router/src/main/kotlin/io/moia/router/ResponseEntity.kt @@ -21,28 +21,40 @@ import java.net.URI data class ResponseEntity( val statusCode: Int, val body: T? = null, - val headers: Map = emptyMap() + val headers: Map = emptyMap(), ) { companion object { - fun ok(body: T? = null, headers: Map = emptyMap()) = - ResponseEntity(200, body, headers) - - fun created(body: T? = null, location: URI? = null, headers: Map = emptyMap()) = - ResponseEntity(201, body, if (location == null) headers else headers + ("location" to location.toString())) - - fun accepted(body: T? = null, headers: Map = emptyMap()) = - ResponseEntity(202, body, headers) - - fun noContent(headers: Map = emptyMap()) = - ResponseEntity(204, null, headers) - - fun badRequest(body: T? = null, headers: Map = emptyMap()) = - ResponseEntity(400, body, headers) - - fun notFound(body: T? = null, headers: Map = emptyMap()) = - ResponseEntity(404, body, headers) - - fun unprocessableEntity(body: T? = null, headers: Map = emptyMap()) = - ResponseEntity(422, body, headers) + fun ok( + body: T? = null, + headers: Map = emptyMap(), + ) = ResponseEntity(200, body, headers) + + fun created( + body: T? = null, + location: URI? = null, + headers: Map = emptyMap(), + ) = ResponseEntity(201, body, if (location == null) headers else headers + ("location" to location.toString())) + + fun accepted( + body: T? = null, + headers: Map = emptyMap(), + ) = ResponseEntity(202, body, headers) + + fun noContent(headers: Map = emptyMap()) = ResponseEntity(204, null, headers) + + fun badRequest( + body: T? = null, + headers: Map = emptyMap(), + ) = ResponseEntity(400, body, headers) + + fun notFound( + body: T? = null, + headers: Map = emptyMap(), + ) = ResponseEntity(404, body, headers) + + fun unprocessableEntity( + body: T? = null, + headers: Map = emptyMap(), + ) = ResponseEntity(422, body, headers) } } diff --git a/router/src/main/kotlin/io/moia/router/Router.kt b/router/src/main/kotlin/io/moia/router/Router.kt index d20f7f47..87c2236d 100644 --- a/router/src/main/kotlin/io/moia/router/Router.kt +++ b/router/src/main/kotlin/io/moia/router/Router.kt @@ -22,8 +22,8 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent typealias PredicateFactory = (String, String, Set, Set) -> RequestPredicate +@Suppress("FunctionName") class Router(private val predicateFactory: PredicateFactory) { - val routes = mutableListOf>() var defaultConsuming = setOf("application/json") @@ -33,55 +33,76 @@ class Router(private val predicateFactory: PredicateFactory) { var filter: Filter = Filter.NoOp - fun GET(pattern: String, handlerFunction: HandlerFunction) = - defaultRequestPredicate(pattern, "GET", handlerFunction, emptySet()) + fun GET( + pattern: String, + handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "GET", handlerFunction, emptySet()) - fun POST(pattern: String, handlerFunction: HandlerFunction) = - defaultRequestPredicate(pattern, "POST", handlerFunction) + fun POST( + pattern: String, + handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "POST", handlerFunction) - fun PUT(pattern: String, handlerFunction: HandlerFunction) = - defaultRequestPredicate(pattern, "PUT", handlerFunction) + fun PUT( + pattern: String, + handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "PUT", handlerFunction) - fun DELETE(pattern: String, handlerFunction: HandlerFunction) = - defaultRequestPredicate(pattern, "DELETE", handlerFunction, emptySet()) + fun DELETE( + pattern: String, + handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "DELETE", handlerFunction, emptySet()) - fun PATCH(pattern: String, handlerFunction: HandlerFunction) = - defaultRequestPredicate(pattern, "PATCH", handlerFunction) + fun PATCH( + pattern: String, + handlerFunction: HandlerFunction, + ) = defaultRequestPredicate(pattern, "PATCH", handlerFunction) private fun defaultRequestPredicate( pattern: String, method: String, handlerFunction: HandlerFunction, - consuming: Set = defaultConsuming + consuming: Set = defaultConsuming, ) = predicateFactory(method, pattern, consuming, defaultProducing) .also { routes += RouterFunction(it, handlerFunction) } companion object { - - fun defaultPredicateFactory(method: String, pattern: String, consuming: Set, producing: Set): RequestPredicate = + fun defaultPredicateFactory( + method: String, + pattern: String, + consuming: Set, + producing: Set, + ): RequestPredicate = RequestPredicateImpl( method = method, pathPattern = pattern, consumes = consuming, - produces = producing + produces = producing, ) + fun router(routes: Router.() -> Unit) = Router(Router::defaultPredicateFactory).apply(routes) - fun router(factory: PredicateFactory, routes: Router.() -> Unit) = Router(factory).apply(routes) + + fun router( + factory: PredicateFactory, + routes: Router.() -> Unit, + ) = Router(factory).apply(routes) } } interface Filter : (APIGatewayRequestHandlerFunction) -> APIGatewayRequestHandlerFunction { companion object { - operator fun invoke(fn: (APIGatewayRequestHandlerFunction) -> APIGatewayRequestHandlerFunction): Filter = object : - Filter { - override operator fun invoke(next: APIGatewayRequestHandlerFunction): APIGatewayRequestHandlerFunction = fn(next) - } + operator fun invoke(fn: (APIGatewayRequestHandlerFunction) -> APIGatewayRequestHandlerFunction): Filter = + object : + Filter { + override operator fun invoke(next: APIGatewayRequestHandlerFunction): APIGatewayRequestHandlerFunction = fn(next) + } } } val Filter.Companion.NoOp: Filter get() = Filter { next -> { next(it) } } fun Filter.then(next: Filter): Filter = Filter { this(next(it)) } + fun Filter.then(next: APIGatewayRequestHandlerFunction): APIGatewayRequestHandlerFunction = { this(next)(it) } typealias APIGatewayRequestHandlerFunction = (APIGatewayProxyRequestEvent) -> APIGatewayProxyResponseEvent @@ -89,7 +110,7 @@ typealias HandlerFunction = (request: Request) -> ResponseEntity class RouterFunction( val requestPredicate: RequestPredicate, - val handler: HandlerFunction + val handler: HandlerFunction, ) { override fun toString(): String { return "RouterFunction(requestPredicate=$requestPredicate)" @@ -97,13 +118,16 @@ class RouterFunction( } data class Request(val apiRequest: APIGatewayProxyRequestEvent, val body: I, val pathPattern: String = apiRequest.path) { - val pathParameters by lazy { UriTemplate.from(pathPattern).extract(apiRequest.path) } val queryParameters: Map? by lazy { apiRequest.queryStringParameters } val multiValueQueryStringParameters: Map>? by lazy { apiRequest.multiValueQueryStringParameters } val requestContext: ProxyRequestContext by lazy { apiRequest.requestContext } + 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) } diff --git a/router/src/main/kotlin/io/moia/router/SerializationHandler.kt b/router/src/main/kotlin/io/moia/router/SerializationHandler.kt index 66e849f8..8941358c 100644 --- a/router/src/main/kotlin/io/moia/router/SerializationHandler.kt +++ b/router/src/main/kotlin/io/moia/router/SerializationHandler.kt @@ -21,38 +21,53 @@ import com.google.common.net.MediaType import isCompatibleWith interface SerializationHandler { + fun supports( + acceptHeader: MediaType, + body: Any, + ): Boolean - fun supports(acceptHeader: MediaType, body: Any): Boolean - - fun serialize(acceptHeader: MediaType, body: Any): String + fun serialize( + acceptHeader: MediaType, + body: Any, + ): String } class SerializationHandlerChain(private val handlers: List) : SerializationHandler { + override fun supports( + acceptHeader: MediaType, + body: Any, + ): Boolean = handlers.any { it.supports(acceptHeader, body) } - override fun supports(acceptHeader: MediaType, body: Any): Boolean = - handlers.any { it.supports(acceptHeader, body) } - - override fun serialize(acceptHeader: MediaType, body: Any): String = - handlers.first { it.supports(acceptHeader, body) }.serialize(acceptHeader, body) + override fun serialize( + acceptHeader: MediaType, + body: Any, + ): String = handlers.first { it.supports(acceptHeader, body) }.serialize(acceptHeader, body) } class JsonSerializationHandler(private val objectMapper: ObjectMapper) : SerializationHandler { - private val json = MediaType.parse("application/json") private val jsonStructuredSuffixWildcard = MediaType.parse("application/*+json") - override fun supports(acceptHeader: MediaType, body: Any): Boolean = - json.isCompatibleWith(acceptHeader) || jsonStructuredSuffixWildcard.isCompatibleWith(acceptHeader) + override fun supports( + acceptHeader: MediaType, + body: Any, + ): Boolean = json.isCompatibleWith(acceptHeader) || jsonStructuredSuffixWildcard.isCompatibleWith(acceptHeader) - override fun serialize(acceptHeader: MediaType, body: Any): String = - objectMapper.writeValueAsString(body) + override fun serialize( + acceptHeader: MediaType, + body: Any, + ): String = objectMapper.writeValueAsString(body) } class PlainTextSerializationHandler(val supportedAcceptTypes: List = listOf(MediaType.parse("text/*"))) : SerializationHandler { - override fun supports(acceptHeader: MediaType, body: Any): Boolean = - supportedAcceptTypes.any { acceptHeader.isCompatibleWith(it) } + override fun supports( + acceptHeader: MediaType, + body: Any, + ): Boolean = supportedAcceptTypes.any { acceptHeader.isCompatibleWith(it) } - override fun serialize(acceptHeader: MediaType, body: Any): String = - body.toString() + override fun serialize( + acceptHeader: MediaType, + body: Any, + ): String = body.toString() } diff --git a/router/src/main/kotlin/io/moia/router/UriTemplate.kt b/router/src/main/kotlin/io/moia/router/UriTemplate.kt index 9882b90a..b7c5bc83 100644 --- a/router/src/main/kotlin/io/moia/router/UriTemplate.kt +++ b/router/src/main/kotlin/io/moia/router/UriTemplate.kt @@ -30,17 +30,18 @@ class UriTemplate private constructor(private val template: String) { } matches = PATH_VARIABLE_REGEX.findAll(template) parameterNames = matches.map { it.groupValues[1] }.toList() - templateRegex = template.replace( - PATH_VARIABLE_REGEX, - { notMatched -> Pattern.quote(notMatched) }, - { matched -> - // check for greedy path variables, e.g. '{proxy+}' - if (matched.groupValues[1].endsWith("+")) { - return@replace "(.+)" - } - if (matched.groupValues[2].isBlank()) "([^/]+)" else "(${matched.groupValues[2]})" - } - ).toRegex() + templateRegex = + template.replace( + PATH_VARIABLE_REGEX, + { notMatched -> Pattern.quote(notMatched) }, + { matched -> + // check for greedy path variables, e.g. '{proxy+}' + if (matched.groupValues[1].endsWith("+")) { + return@replace "(.+)" + } + if (matched.groupValues[2].isBlank()) "([^/]+)" else "(${matched.groupValues[2]})" + }, + ).toRegex() } companion object { @@ -60,7 +61,11 @@ class UriTemplate private constructor(private val template: String) { private fun Regex.findParameterValues(uri: String): List = findAll(uri).first().groupValues.drop(1).map { URLDecoder.decode(it, "UTF-8") } - private fun String.replace(regex: Regex, notMatched: (String) -> String, matched: (MatchResult) -> String): String { + private fun String.replace( + regex: Regex, + notMatched: (String) -> String, + matched: (MatchResult) -> String, + ): String { val matches = regex.findAll(this) val builder = StringBuilder() var position = 0 diff --git a/router/src/test/kotlin/io/moia/router/APIGatewayProxyEventExtensionsTest.kt b/router/src/test/kotlin/io/moia/router/APIGatewayProxyEventExtensionsTest.kt index 669cfe3c..ea4711d3 100644 --- a/router/src/test/kotlin/io/moia/router/APIGatewayProxyEventExtensionsTest.kt +++ b/router/src/test/kotlin/io/moia/router/APIGatewayProxyEventExtensionsTest.kt @@ -22,16 +22,17 @@ import org.assertj.core.api.BDDAssertions.then import org.junit.jupiter.api.Test class APIGatewayProxyEventExtensionsTest { - @Test fun `should add location header`() { - val request = GET() - .withHeader("Host", "example.com") - .withHeader("X-Forwarded-Proto", "http") - .withHeader("X-Forwarded-Port", "8080") + val request = + GET() + .withHeader("Host", "example.com") + .withHeader("X-Forwarded-Proto", "http") + .withHeader("X-Forwarded-Port", "8080") - val response = APIGatewayProxyResponseEvent() - .withLocationHeader(request, "/some/path") + val response = + APIGatewayProxyResponseEvent() + .withLocationHeader(request, "/some/path") then(response.location()).isEqualTo("http://example.com:8080/some/path") } @@ -40,18 +41,20 @@ class APIGatewayProxyEventExtensionsTest { fun `should add location header with default host and proto and without port`() { val request = GET() - val response = APIGatewayProxyResponseEvent() - .withLocationHeader(request, "/some/path") + val response = + APIGatewayProxyResponseEvent() + .withLocationHeader(request, "/some/path") then(response.location()).isEqualTo("http://localhost/some/path") } @Test fun `should omit default https port`() { - val request = GET() - .withHeader("Host", "example.com") - .withHeader("X-Forwarded-Proto", "https") - .withHeader("X-Forwarded-Port", "443") + val request = + GET() + .withHeader("Host", "example.com") + .withHeader("X-Forwarded-Proto", "https") + .withHeader("X-Forwarded-Port", "443") val location = request.location("some/path") @@ -60,10 +63,11 @@ class APIGatewayProxyEventExtensionsTest { @Test fun `should omit default http port`() { - val request = GET() - .withHeader("Host", "example.com") - .withHeader("X-Forwarded-Proto", "http") - .withHeader("X-Forwarded-Port", "80") + val request = + GET() + .withHeader("Host", "example.com") + .withHeader("X-Forwarded-Proto", "http") + .withHeader("X-Forwarded-Port", "80") val location = request.location("/some/path") diff --git a/router/src/test/kotlin/io/moia/router/ApiRequestTest.kt b/router/src/test/kotlin/io/moia/router/ApiRequestTest.kt index c6c6d8a0..58798921 100644 --- a/router/src/test/kotlin/io/moia/router/ApiRequestTest.kt +++ b/router/src/test/kotlin/io/moia/router/ApiRequestTest.kt @@ -22,7 +22,6 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent import org.junit.jupiter.api.Test class ApiRequestTest { - @Test fun `should match header`() { val request = APIGatewayProxyRequestEvent().withHeaders(mapOf("Accept" to "application/json")) diff --git a/router/src/test/kotlin/io/moia/router/JsonDeserializationHandlerTest.kt b/router/src/test/kotlin/io/moia/router/JsonDeserializationHandlerTest.kt index 86706554..6ade7d8a 100644 --- a/router/src/test/kotlin/io/moia/router/JsonDeserializationHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/JsonDeserializationHandlerTest.kt @@ -23,7 +23,6 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class JsonDeserializationHandlerTest { - val deserializationHandler = JsonDeserializationHandler(jacksonObjectMapper()) @Test @@ -31,14 +30,14 @@ class JsonDeserializationHandlerTest { assertTrue( deserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "application/json") - ) + .withHeader("content-type", "application/json"), + ), ) assertTrue( deserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "application/vnd.moia.v1+json") - ) + .withHeader("content-type", "application/vnd.moia.v1+json"), + ), ) } @@ -47,14 +46,14 @@ class JsonDeserializationHandlerTest { assertFalse( deserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "image/png") - ) + .withHeader("content-type", "image/png"), + ), ) assertFalse( deserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "text/plain") - ) + .withHeader("content-type", "text/plain"), + ), ) } @@ -63,14 +62,14 @@ class JsonDeserializationHandlerTest { assertTrue( deserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "application/json; charset=UTF-8") - ) + .withHeader("content-type", "application/json; charset=UTF-8"), + ), ) assertTrue( deserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "application/vnd.moia.v1+json; charset=UTF-8") - ) + .withHeader("content-type", "application/vnd.moia.v1+json; charset=UTF-8"), + ), ) } @@ -79,14 +78,14 @@ class JsonDeserializationHandlerTest { assertFalse( deserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "application/json; charset=UTF-16") - ) + .withHeader("content-type", "application/json; charset=UTF-16"), + ), ) assertFalse( deserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "application/vnd.moia.v1+json; charset=UTF-16") - ) + .withHeader("content-type", "application/vnd.moia.v1+json; charset=UTF-16"), + ), ) } } diff --git a/router/src/test/kotlin/io/moia/router/JwtPermissionHandlerTest.kt b/router/src/test/kotlin/io/moia/router/JwtPermissionHandlerTest.kt index c0152a21..a79e6f34 100644 --- a/router/src/test/kotlin/io/moia/router/JwtPermissionHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/JwtPermissionHandlerTest.kt @@ -20,8 +20,8 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent import org.assertj.core.api.BDDAssertions.then import org.junit.jupiter.api.Test +@Suppress("ktlint:standard:max-line-length") class JwtPermissionHandlerTest { - /* { "sub": "1234567890", @@ -29,7 +29,7 @@ class JwtPermissionHandlerTest { "iat": 1516239022, "scope": "one two" } - */ + */ val jwtWithScopeClaimSpace = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6Im9uZSB0d28ifQ.2tPrDymXDejHfVjNlVh4XUj22ZuDrKHP6dvWN7JNAWY" /* @@ -58,14 +58,16 @@ class JwtPermissionHandlerTest { @Test fun `should extract permissions from custom permissions claim`() { - val handler = JwtPermissionHandler( - accessor = JwtAccessor( - APIGatewayProxyRequestEvent() - .withHeader("Authorization", jwtWithCustomClaimAndSeparator) - ), - permissionsClaim = "userRights", - permissionSeparator = "," - ) + val handler = + JwtPermissionHandler( + accessor = + JwtAccessor( + APIGatewayProxyRequestEvent() + .withHeader("Authorization", jwtWithCustomClaimAndSeparator), + ), + permissionsClaim = "userRights", + permissionSeparator = ",", + ) thenRecognizesRequiredPermissions(handler) } @@ -103,7 +105,7 @@ class JwtPermissionHandlerTest { JwtPermissionHandler( JwtAccessor( APIGatewayProxyRequestEvent() - .withHeader("Authorization", authHeader) - ) + .withHeader("Authorization", authHeader), + ), ) } diff --git a/router/src/test/kotlin/io/moia/router/MediaTypeTest.kt b/router/src/test/kotlin/io/moia/router/MediaTypeTest.kt index 3b0d0d43..f289ba4d 100644 --- a/router/src/test/kotlin/io/moia/router/MediaTypeTest.kt +++ b/router/src/test/kotlin/io/moia/router/MediaTypeTest.kt @@ -22,7 +22,6 @@ import org.assertj.core.api.BDDAssertions.then import org.junit.jupiter.api.Test class MediaTypeTest { - @Test fun `should match`() { then(MediaType.parse("application/json").isCompatibleWith(MediaType.parse("application/json"))).isTrue() diff --git a/router/src/test/kotlin/io/moia/router/NoOpPermissionHandlerTest.kt b/router/src/test/kotlin/io/moia/router/NoOpPermissionHandlerTest.kt index bebc569f..51f7d0bf 100644 --- a/router/src/test/kotlin/io/moia/router/NoOpPermissionHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/NoOpPermissionHandlerTest.kt @@ -20,7 +20,6 @@ import org.assertj.core.api.BDDAssertions.then import org.junit.jupiter.api.Test class NoOpPermissionHandlerTest { - @Test fun `should always return true`() { val handler = NoOpPermissionHandler() diff --git a/router/src/test/kotlin/io/moia/router/PlainTextDeserializationHandlerTest.kt b/router/src/test/kotlin/io/moia/router/PlainTextDeserializationHandlerTest.kt index 6733fe0c..a9cae07d 100644 --- a/router/src/test/kotlin/io/moia/router/PlainTextDeserializationHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/PlainTextDeserializationHandlerTest.kt @@ -23,20 +23,19 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class PlainTextDeserializationHandlerTest { - @Test fun `should support text`() { assertTrue( PlainTextDeserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "text/plain") - ) + .withHeader("content-type", "text/plain"), + ), ) assertTrue( PlainTextDeserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "text/csv") - ) + .withHeader("content-type", "text/csv"), + ), ) } @@ -45,14 +44,14 @@ class PlainTextDeserializationHandlerTest { assertFalse( PlainTextDeserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "image/png") - ) + .withHeader("content-type", "image/png"), + ), ) assertFalse( PlainTextDeserializationHandler.supports( APIGatewayProxyRequestEvent() - .withHeader("content-type", "application/json") - ) + .withHeader("content-type", "application/json"), + ), ) } diff --git a/router/src/test/kotlin/io/moia/router/PlainTextSerializationHandlerTest.kt b/router/src/test/kotlin/io/moia/router/PlainTextSerializationHandlerTest.kt index 82039624..bab7ed17 100644 --- a/router/src/test/kotlin/io/moia/router/PlainTextSerializationHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/PlainTextSerializationHandlerTest.kt @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class PlainTextSerializationHandlerTest { - @Test fun `should support text`() { assertTrue(PlainTextSerializationHandler().supports(MediaType.parse("text/plain"), "some")) diff --git a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt index 6acbc030..b51982bc 100644 --- a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt @@ -31,20 +31,21 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.time.LocalDate +@Suppress("ktlint:standard:max-line-length") class RequestHandlerTest { - private val testRequestHandler = TestRequestHandler() private val mapper = testRequestHandler.objectMapper @Test fun `should match request`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""{"greeting":"Hello"}""") @@ -52,13 +53,14 @@ class RequestHandlerTest { @Test fun `should match request with path parameter`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some/me") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some/me") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""{"greeting":"Hello me"}""") @@ -66,48 +68,51 @@ class RequestHandlerTest { @Test fun `should return not acceptable on unsupported accept header`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "image/jpg")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "image/jpg")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(406) } @Test fun `should return unsupported media type`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some") - .withHttpMethod("POST") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Content-Type" to "image/jpg" - ) - ), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some") + .withHttpMethod("POST") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "image/jpg", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(415) } @Test fun `should handle request with body`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/json", + ), ) - ) - .withBody("""{ "greeting": "some" }"""), - mockk() - ) + .withBody("""{ "greeting": "some" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""{"greeting":"some"}""") @@ -115,17 +120,18 @@ class RequestHandlerTest { @Test fun `should handle request with body as a List`() { - val response = testRequestHandler.handleRequest( - POST("/somes") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/somes") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/json", + ), ) - ) - .withBody("""[{ "greeting": "some" },{ "greeting": "some1" }]""".trimMargin()), - mockk() - ) + .withBody("""[{ "greeting": "some" },{ "greeting": "some1" }]""".trimMargin()), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""[{"greeting":"some"},{"greeting":"some1"}]""") @@ -133,31 +139,33 @@ class RequestHandlerTest { @Test fun `should return method not allowed`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some") - .withHttpMethod("PUT") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Content-Type" to "image/jpg" - ) - ), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some") + .withHttpMethod("PUT") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "image/jpg", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(405) } @Test fun `should return not found`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some-other") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some-other") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(404) } @@ -165,13 +173,14 @@ class RequestHandlerTest { @Test fun `should invoke filter chain`() { val handler = TestRequestHandlerWithFilter() - val response = handler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")), - mockk() - ) + val response = + handler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(handler.filterInvocations).isEqualTo(2) @@ -180,13 +189,14 @@ class RequestHandlerTest { @Test fun `should invoke filter chain also for non successful requests`() { val handler = TestRequestHandlerWithFilter() - val response = handler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some-internal-server-error") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")), - mockk() - ) + val response = + handler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some-internal-server-error") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(500) assertThat(response.headers["header"]).isEqualTo("value") @@ -196,51 +206,54 @@ class RequestHandlerTest { @Test fun `should ignore content-type header when handler expects none`() { val handler = TestRequestHandlerWithFilter() - val response = handler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some") - .withHttpMethod("GET") - .withHeaders( - mapOf( - "Accept" to "application/json", - "content-type" to "application/json" - ) - ), - mockk() - ) + val response = + handler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some") + .withHttpMethod("GET") + .withHeaders( + mapOf( + "Accept" to "application/json", + "content-type" to "application/json", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) } @Test fun `should handle deserialization error`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/json", + ), ) - ) - .withBody("{}"), - mockk() - ) + .withBody("{}"), + mockk(), + ) assertThat(response.statusCode).isEqualTo(422) } @Test fun `should handle deserialization error, when field has invalid format`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/json", + ), ) - ) - .withBody("""{"greeting": "hello","age": "a"}"""), - mockk() - ) + .withBody("""{"greeting": "hello","age": "a"}"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(422) val body = mapper.readValue>(response.body) assertThat(body.size).isEqualTo(1) @@ -254,17 +267,18 @@ class RequestHandlerTest { @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" + 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() - ) + .withBody("""{"greeting": "hello","age": 1, "bday": "2000-01-AA"}"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(422) val body = mapper.readValue>(response.body) assertThat(body.size).isEqualTo(1) @@ -278,17 +292,18 @@ class RequestHandlerTest { @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" + 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() - ) + .withBody("""{"greeting": "hello", bday: "2000-01-01"}"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(422) val body = mapper.readValue>(response.body) assertThat(body.size).isEqualTo(1) @@ -302,77 +317,82 @@ class RequestHandlerTest { @Test fun `should return 400 on missing body when content type stated`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/json", + ), ) - ) - .withBody(null), - mockk() - ) + .withBody(null), + mockk(), + ) assertThat(response.statusCode).isEqualTo(400) assertThat(mapper.readValue(response.body).code).isEqualTo("REQUEST_BODY_MISSING") } @Test fun `should handle null body when content type is stated and request handler body type is nullable`() { - val response = testRequestHandler.handleRequest( - POST("/some-nullable") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some-nullable") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Content-Type" to "application/json", + ), ) - ) - .withBody(null), - mockk() - ) + .withBody(null), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""{"greeting":""}""") } @Test fun `should handle api exception`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some-api-exception") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some-api-exception") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(400) } @Test fun `should handle internal server error`() { - val response = testRequestHandler.handleRequest( - APIGatewayProxyRequestEvent() - .withPath("/some-internal-server-error") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")), - mockk() - ) + val response = + testRequestHandler.handleRequest( + APIGatewayProxyRequestEvent() + .withPath("/some-internal-server-error") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")), + mockk(), + ) assertThat(response.statusCode).isEqualTo(500) } @Test fun `should handle request with a media type range in accept header`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "application/xhtml+xml, application/json, application/xml;q=0.9, image/webp, */*;q=0.8", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/xhtml+xml, application/json, application/xml;q=0.9, image/webp, */*;q=0.8", + "Content-Type" to "application/json", + ), ) - ) - .withBody("""{ "greeting": "some" }"""), - mockk() - ) + .withBody("""{ "greeting": "some" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.getHeaderCaseInsensitive("content-type")).isEqualTo("application/json") @@ -382,17 +402,18 @@ class RequestHandlerTest { @Test fun `should handle request with accept all header`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "*/*", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "*/*", + "Content-Type" to "application/json", + ), ) - ) - .withBody("""{ "greeting": "some" }"""), - mockk() - ) + .withBody("""{ "greeting": "some" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.getHeaderCaseInsensitive("content-type")).isEqualTo("application/vnd.moia.v2+json") @@ -402,17 +423,18 @@ class RequestHandlerTest { @Test fun `should handle subtype structured suffix wildcard`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "application/vnd.moia.v1+json", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/vnd.moia.v1+json", + "Content-Type" to "application/json", + ), ) - ) - .withBody("""{ "greeting": "some" }"""), - mockk() - ) + .withBody("""{ "greeting": "some" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""{"greeting":"some"}""") @@ -420,17 +442,18 @@ class RequestHandlerTest { @Test fun `should match version`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "application/vnd.moia.v2+json", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "application/vnd.moia.v2+json", + "Content-Type" to "application/json", + ), ) - ) - .withBody("""{ "greeting": "v2" }"""), - mockk() - ) + .withBody("""{ "greeting": "v2" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) assertThat(response.body).isEqualTo("""{"greeting":"v2"}""") @@ -439,122 +462,131 @@ class RequestHandlerTest { @Test fun `should fail with 406 Not Acceptable on an unparsable media type`() { - val response = testRequestHandler.handleRequest( - POST("/some") - .withHeaders( - mapOf( - "Accept" to "*", - "Content-Type" to "application/json" + val response = + testRequestHandler.handleRequest( + POST("/some") + .withHeaders( + mapOf( + "Accept" to "*", + "Content-Type" to "application/json", + ), ) - ) - .withBody("""{ "greeting": "some" }"""), - mockk() - ) + .withBody("""{ "greeting": "some" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(406) } @Test fun `should match request requiring permission`() { - val response = TestRequestHandlerAuthorization().handleRequest( - GET("/some") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Authorization" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24xIn0.E3PxWx68uP2s9yyAV7UVs8egyrGTIuWXjtkcqAA840I" - ) - ), - mockk() - ) + val response = + TestRequestHandlerAuthorization().handleRequest( + GET("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Authorization" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24xIn0.E3PxWx68uP2s9yyAV7UVs8egyrGTIuWXjtkcqAA840I", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) } @Test fun `should match request requiring permission from custom header`() { - val response = TestRequestHandlerCustomAuthorizationHeader().handleRequest( - GET("/some") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Custom-Auth" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24xIn0.E3PxWx68uP2s9yyAV7UVs8egyrGTIuWXjtkcqAA840I" - ) - ), - mockk() - ) + val response = + TestRequestHandlerCustomAuthorizationHeader().handleRequest( + GET("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Custom-Auth" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24xIn0.E3PxWx68uP2s9yyAV7UVs8egyrGTIuWXjtkcqAA840I", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(200) } @Test fun `should fail on missing permission`() { - val response = TestRequestHandlerAuthorization().handleRequest( - GET("/some") - .withHeaders( - mapOf( - "Accept" to "application/json", - "Authorization" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24yIn0.RA8ERppuFmastqFN-6C98WqMEE7L6h88WylMeq6jh1w" - ) - ), - mockk() - ) + val response = + TestRequestHandlerAuthorization().handleRequest( + GET("/some") + .withHeaders( + mapOf( + "Accept" to "application/json", + "Authorization" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24yIn0.RA8ERppuFmastqFN-6C98WqMEE7L6h88WylMeq6jh1w", + ), + ), + mockk(), + ) assertThat(response.statusCode).isEqualTo(403) } @Test fun `Request without headers should return status code 406`() { - val response = testRequestHandler.handleRequest( - GET("/some"), - mockk() - ) + val response = + testRequestHandler.handleRequest( + GET("/some"), + mockk(), + ) assertThat(response.statusCode).isEqualTo(406) } @Test fun `Request without request path should return status code 404`() { - val response = testRequestHandler.handleRequest( - GET(), - mockk() - ) + val response = + testRequestHandler.handleRequest( + GET(), + mockk(), + ) assertThat(response.statusCode).isEqualTo(404) } @Test fun `Successful POST request should return status code 204`() { - val response = testRequestHandler.handleRequest( - POST("/no-content") - .withHeader("Accept", "application/json") - .withHeader("Content-Type", "application/json") - .withBody("""{ "greeting": "some" }"""), - mockk() - ) + val response = + testRequestHandler.handleRequest( + POST("/no-content") + .withHeader("Accept", "application/json") + .withHeader("Content-Type", "application/json") + .withBody("""{ "greeting": "some" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(204) assertThat(response.body).isNullOrEmpty() } @Test fun `Create should not return a location header`() { - val response = testRequestHandler.handleRequest( - POST("/create-without-location") - .withHeader("Accept", "application/json") - .withHeader("Content-Type", "application/json") - .withBody("""{ "greeting": "some" }"""), - mockk() - ) + val response = + testRequestHandler.handleRequest( + POST("/create-without-location") + .withHeader("Accept", "application/json") + .withHeader("Content-Type", "application/json") + .withBody("""{ "greeting": "some" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(201) assertThat(response.headers.containsKey("location")).isFalse() } @Test fun `Create should return a location header`() { - val response = testRequestHandler.handleRequest( - POST("/create-with-location") - .withHeader("Accept", "application/json") - .withHeader("Content-Type", "application/json") - .withBody("""{ "greeting": "some" }"""), - mockk() - ) + val response = + testRequestHandler.handleRequest( + POST("/create-with-location") + .withHeader("Accept", "application/json") + .withHeader("Content-Type", "application/json") + .withBody("""{ "greeting": "some" }"""), + mockk(), + ) assertThat(response.statusCode).isEqualTo(201) assertThat(response.headers.containsKey("location")).isTrue() assertThat(response.headers["location"]).isEqualTo("http://localhost/test") @@ -562,13 +594,14 @@ class RequestHandlerTest { @Test fun `Deletion should ignore the body and content-type`() { - val response = testRequestHandler.handleRequest( - DELETE("/delete-me") - .withHeader("Accept", "application/json") - .withHeader("Content-Type", "text/csv") - .withBody("this may be faulty"), - mockk() - ) + val response = + testRequestHandler.handleRequest( + DELETE("/delete-me") + .withHeader("Accept", "application/json") + .withHeader("Content-Type", "text/csv") + .withBody("this may be faulty"), + mockk(), + ) assertThat(response.statusCode).isEqualTo(204) } @@ -578,48 +611,54 @@ class RequestHandlerTest { GET("/search") .withQueryStringParameters( mapOf( - "testQueryParam" to "foo" - ) + "testQueryParam" to "foo", + ), ) .withMultiValueQueryStringParameters( mapOf( - "testMultiValueQueryStringParam" to listOf("foo", "bar") - ) + "testMultiValueQueryStringParam" to listOf("foo", "bar"), + ), ), - mockk() + mockk(), ) TestQueryParamParsingHandler().handleRequest( GET("/search?testQueryParam=foo&testMultiValueQueryStringParam=foo&testMultiValueQueryStringParam=bar"), - mockk() + mockk(), ) } @Test fun `Not existing path parameter should throw an error`() { - val response = testRequestHandler.handleRequest( - GET("/non-existing-path-parameter") - .withHeader("accept", "application/json"), - mockk() - ) + val response = + testRequestHandler.handleRequest( + GET("/non-existing-path-parameter") + .withHeader("accept", "application/json"), + mockk(), + ) assertEquals(500, response.statusCode) - assertEquals("{\"message\":\"Could not find path parameter 'foo\",\"code\":\"INTERNAL_SERVER_ERROR\",\"details\":{}}", response.body) + assertEquals( + "{\"message\":\"Could not find path parameter 'foo\",\"code\":\"INTERNAL_SERVER_ERROR\",\"details\":{}}", + response.body, + ) } @Test fun `should return the content type that is accepted`() { - val jsonResponse = AcceptTypeDependingHandler().handleRequest( - GET("/all-objects") - .withHeader("accept", "application/json"), - mockk() - ) + val jsonResponse = + AcceptTypeDependingHandler().handleRequest( + GET("/all-objects") + .withHeader("accept", "application/json"), + mockk(), + ) assertEquals(200, jsonResponse.statusCode) assertEquals("application/json", jsonResponse.getHeaderCaseInsensitive("content-type")) assertEquals("[{\"text\":\"foo\",\"number\":1},{\"text\":\"bar\",\"number\":2}]", jsonResponse.body) - val plainTextResponse = AcceptTypeDependingHandler().handleRequest( - GET("/all-objects") - .withHeader("accept", "text/plain"), - mockk() - ) + val plainTextResponse = + AcceptTypeDependingHandler().handleRequest( + GET("/all-objects") + .withHeader("accept", "text/plain"), + mockk(), + ) assertEquals(200, plainTextResponse.statusCode) assertEquals("text/plain", plainTextResponse.getHeaderCaseInsensitive("content-type")) assertEquals("[CustomObject(text=foo, number=1), CustomObject(text=bar, number=2)]", plainTextResponse.body) @@ -627,21 +666,22 @@ class RequestHandlerTest { @Test fun `headers should be case insensitive`() { - val request = APIGatewayProxyRequestEvent() - .withPath("/some") - .withHttpMethod("GET") - .withHeaders( - mapOf( - "Accept" to "Application/Json", - "User-Agent" to "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" + val request = + APIGatewayProxyRequestEvent() + .withPath("/some") + .withHttpMethod("GET") + .withHeaders( + mapOf( + "Accept" to "Application/Json", + "User-Agent" to "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + ), ) - ) val response = testRequestHandler.handleRequest(request, mockk()) assertThat(request.headers["accept"].toString()).isEqualTo("Application/Json") assertThat(request.headers["user-agent"].toString()) .isEqualTo( - "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", ) assertThat(response.statusCode).isEqualTo(200) } @@ -649,16 +689,18 @@ class RequestHandlerTest { @Test fun `should deserialize plain text`() { class SampleRouter : RequestHandler() { - override val router = router { - POST("/some", { r: Request -> ResponseEntity.ok(r.body) }) - .producing("text/plain") - .consuming("text/plain") - } + override val router = + router { + POST("/some", { r: Request -> ResponseEntity.ok(r.body) }) + .producing("text/plain") + .consuming("text/plain") + } } - val request = POST("/some") - .withAcceptHeader("text/plain") - .withContentTypeHeader("text/plain") - .withBody("just text") + val request = + POST("/some") + .withAcceptHeader("text/plain") + .withContentTypeHeader("text/plain") + .withBody("just text") val response = SampleRouter().handleRequest(request, mockk()) @@ -670,14 +712,18 @@ class RequestHandlerTest { @Test fun `should fail for function references when using Kotlin 1_6_10`() { class DummyHandler : RequestHandler() { - val dummy = object { - @Suppress("UNUSED_PARAMETER") - fun handler(r: Request) = ResponseEntity.ok(Unit) - } + val dummy = + object { + @Suppress("UNUSED_PARAMETER") + fun handler(r: Request) = ResponseEntity.ok(Unit) + } + override fun exceptionToResponseEntity(ex: Exception) = throw ex - override val router = router { - GET("/some", dummy::handler).producing("application/json") - } + + override val router = + router { + GET("/some", dummy::handler).producing("application/json") + } } assertThrows { DummyHandler().handleRequest( @@ -685,175 +731,180 @@ class RequestHandlerTest { .withHttpMethod("GET") .withPath("/some") .withAcceptHeader("application/json"), - mockk() + mockk(), ) } } class TestRequestHandlerAuthorization : RequestHandler() { - override val router = router { - GET("/some") { _: Request -> - ResponseEntity.ok("hello") - }.requiringPermissions("permission1") - } + override val router = + router { + GET("/some") { _: Request -> + ResponseEntity.ok("hello") + }.requiringPermissions("permission1") + } override fun permissionHandlerSupplier(): (r: APIGatewayProxyRequestEvent) -> PermissionHandler = { JwtPermissionHandler( request = it, permissionsClaim = "permissions", - permissionSeparator = "," + permissionSeparator = ",", ) } } class TestRequestHandlerCustomAuthorizationHeader : RequestHandler() { - override val router = router { - GET("/some") { _: Request -> - ResponseEntity.ok("hello") - }.requiringPermissions("permission1") - } + override val router = + router { + GET("/some") { _: Request -> + ResponseEntity.ok("hello") + }.requiringPermissions("permission1") + } override fun permissionHandlerSupplier(): (r: APIGatewayProxyRequestEvent) -> PermissionHandler = { JwtPermissionHandler( - accessor = JwtAccessor( - request = it, - authorizationHeaderName = "custom-auth" - ), + accessor = + JwtAccessor( + request = it, + authorizationHeaderName = "custom-auth", + ), permissionsClaim = "permissions", - permissionSeparator = "," + permissionSeparator = ",", ) } } class TestRequestHandlerWithFilter : RequestHandler() { - var filterInvocations = 0 - private val incrementingFilter = Filter { next -> - { request -> - filterInvocations += 1 - next(request).apply { withHeader("header", "value") } + private val incrementingFilter = + Filter { next -> + { request -> + filterInvocations += 1 + next(request).apply { withHeader("header", "value") } + } } - } - override val router = router { - filter = incrementingFilter.then(incrementingFilter) - - GET("/some") { _: Request -> - ResponseEntity.ok("hello") + override val router = + router { + filter = incrementingFilter.then(incrementingFilter) + + GET("/some") { _: Request -> + ResponseEntity.ok("hello") + } + GET("/some-internal-server-error") { + throw IllegalArgumentException("boom") + } } - GET("/some-internal-server-error") { - throw IllegalArgumentException("boom") - } - } } class TestRequestHandler : RequestHandler() { - data class TestResponse(val greeting: String) + data class TestRequest(val greeting: String, val age: Int = 0, val bday: LocalDate = LocalDate.now()) - override val router = router { - GET("/some") { _: Request -> - ResponseEntity.ok( - TestResponse( - "Hello" - ) - ) - } - GET("/some-api-exception") { - throw ApiException("boom", "BOOM", 400, mapOf("more" to "info")) - } - GET("/some-internal-server-error") { - throw IllegalArgumentException("boom") - } - GET("/some/{id}") { r: Request -> - assertThat(r.pathParameters.containsKey("id")).isTrue() - ResponseEntity.ok( - TestResponse( - "Hello ${r.getPathParameter("id")}" + override val router = + router { + GET("/some") { _: Request -> + ResponseEntity.ok( + TestResponse( + "Hello", + ), ) - ) - } - - POST("/some") { _: Request -> - ResponseEntity.ok( - TestResponse( - "v2" + } + GET("/some-api-exception") { + throw ApiException("boom", "BOOM", 400, mapOf("more" to "info")) + } + GET("/some-internal-server-error") { + throw IllegalArgumentException("boom") + } + GET("/some/{id}") { r: Request -> + assertThat(r.pathParameters.containsKey("id")).isTrue() + ResponseEntity.ok( + TestResponse( + "Hello ${r.getPathParameter("id")}", + ), ) - ) - }.producing("application/vnd.moia.v2+json") + } - POST("/some") { r: Request -> - ResponseEntity.ok( - TestResponse( - r.body.greeting + POST("/some") { _: Request -> + ResponseEntity.ok( + TestResponse( + "v2", + ), ) - ) - }.producing("application/json", "application/*+json") + }.producing("application/vnd.moia.v2+json") - POST("/some-nullable") { r: Request -> - ResponseEntity.ok( - TestResponse( - r.body?.greeting.orEmpty() + POST("/some") { r: Request -> + ResponseEntity.ok( + TestResponse( + r.body.greeting, + ), ) - ) - }.producing("application/json") + }.producing("application/json", "application/*+json") - POST("/somes") { r: Request> -> - ResponseEntity.ok( - r.body.map { + POST("/some-nullable") { r: Request -> + ResponseEntity.ok( TestResponse( - it.greeting - ) - }.toList() - ) - } - POST("/no-content") { _: Request -> - ResponseEntity.noContent() - } - POST("/create-without-location") { _: Request -> - ResponseEntity.created(null, null, emptyMap()) - } - POST("/create-with-location") { r: Request -> - ResponseEntity.created(null, r.apiRequest.location("test"), emptyMap()) - } - DELETE("/delete-me") { _: Request -> - ResponseEntity.noContent() - } - GET("/non-existing-path-parameter") { request: Request -> - request.getPathParameter("foo") - ResponseEntity.ok(null) + r.body?.greeting.orEmpty(), + ), + ) + }.producing("application/json") + + POST("/somes") { r: Request> -> + ResponseEntity.ok( + r.body.map { + TestResponse( + it.greeting, + ) + }.toList(), + ) + } + POST("/no-content") { _: Request -> + ResponseEntity.noContent() + } + POST("/create-without-location") { _: Request -> + ResponseEntity.created(null, null, emptyMap()) + } + POST("/create-with-location") { r: Request -> + ResponseEntity.created(null, r.apiRequest.location("test"), emptyMap()) + } + DELETE("/delete-me") { _: Request -> + ResponseEntity.noContent() + } + GET("/non-existing-path-parameter") { request: Request -> + request.getPathParameter("foo") + ResponseEntity.ok(null) + } } - } } class TestQueryParamParsingHandler : RequestHandler() { - - override val router = router { - GET("/search") { r: Request -> - assertThat(r.getQueryParameter("testQueryParam")).isNotNull() - assertThat(r.getQueryParameter("testQueryParam")).isEqualTo("foo") - assertThat(r.queryParameters!!["testQueryParam"]).isNotNull() - assertThat(r.getMultiValueQueryStringParameter("testMultiValueQueryStringParam")).isNotNull() - assertThat(r.getMultiValueQueryStringParameter("testMultiValueQueryStringParam")).isEqualTo(listOf("foo", "bar")) - assertThat(r.multiValueQueryStringParameters!!["testMultiValueQueryStringParam"]).isNotNull() - ResponseEntity.ok(null) + override val router = + router { + GET("/search") { r: Request -> + assertThat(r.getQueryParameter("testQueryParam")).isNotNull() + assertThat(r.getQueryParameter("testQueryParam")).isEqualTo("foo") + assertThat(r.queryParameters!!["testQueryParam"]).isNotNull() + assertThat(r.getMultiValueQueryStringParameter("testMultiValueQueryStringParam")).isNotNull() + assertThat(r.getMultiValueQueryStringParameter("testMultiValueQueryStringParam")).isEqualTo(listOf("foo", "bar")) + assertThat(r.multiValueQueryStringParameters!!["testMultiValueQueryStringParam"]).isNotNull() + ResponseEntity.ok(null) + } } - } } class AcceptTypeDependingHandler : RequestHandler() { - data class CustomObject(val text: String, val number: Int) - override val router = router { - defaultConsuming = setOf("application/json", "text/plain") - defaultProducing = setOf("application/json", "text/plain") - GET("/all-objects") { _: Request -> - ResponseEntity.ok(body = listOf(CustomObject("foo", 1), CustomObject("bar", 2))) + override val router = + router { + defaultConsuming = setOf("application/json", "text/plain") + defaultProducing = setOf("application/json", "text/plain") + GET("/all-objects") { _: Request -> + ResponseEntity.ok(body = listOf(CustomObject("foo", 1), CustomObject("bar", 2))) + } } - } } } diff --git a/router/src/test/kotlin/io/moia/router/ResponseEntityTest.kt b/router/src/test/kotlin/io/moia/router/ResponseEntityTest.kt index 7afc6ee3..47d844e6 100644 --- a/router/src/test/kotlin/io/moia/router/ResponseEntityTest.kt +++ b/router/src/test/kotlin/io/moia/router/ResponseEntityTest.kt @@ -24,11 +24,11 @@ import assertk.assertions.isNull import org.junit.jupiter.api.Test class ResponseEntityTest { - private val body = "body" - private val headers = mapOf( - "content-type" to "text/plain" - ) + private val headers = + mapOf( + "content-type" to "text/plain", + ) @Test fun `should process ok response`() { diff --git a/router/src/test/kotlin/io/moia/router/RouterTest.kt b/router/src/test/kotlin/io/moia/router/RouterTest.kt index 46278829..e75b218c 100644 --- a/router/src/test/kotlin/io/moia/router/RouterTest.kt +++ b/router/src/test/kotlin/io/moia/router/RouterTest.kt @@ -27,14 +27,14 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class RouterTest { - @Test fun `should register get route with default accept header`() { - val router = router { - GET("/some") { r: Request -> - ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") + val router = + router { + GET("/some") { r: Request -> + ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") + } } - } assertThat(router.routes).hasSize(1) with(router.routes.first().requestPredicate) { @@ -47,25 +47,27 @@ class RouterTest { @Test fun `should register routes`() { - val router = router { - PUT("/some") { _: Request -> ResponseEntity.ok("") } - PATCH("/some") { _: Request -> ResponseEntity.ok("") } - DELETE("/some") { _: Request -> ResponseEntity.ok("") } - POST("/some") { _: Request -> ResponseEntity.ok("") } - } + val router = + router { + PUT("/some") { _: Request -> ResponseEntity.ok("") } + PATCH("/some") { _: Request -> ResponseEntity.ok("") } + DELETE("/some") { _: Request -> ResponseEntity.ok("") } + POST("/some") { _: Request -> ResponseEntity.ok("") } + } then(router.routes.map { it.requestPredicate.method }).containsOnly("PUT", "PATCH", "DELETE", "POST") } @Test fun `should register post route with specific content types`() { - val router = router { - POST("/some") { r: Request -> - ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") + val router = + router { + POST("/some") { r: Request -> + ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") + } + .producing("text/plain") + .consuming("text/plain") } - .producing("text/plain") - .consuming("text/plain") - } assertThat(router.routes).hasSize(1) with(router.routes.first().requestPredicate) { @@ -78,14 +80,15 @@ class RouterTest { @Test fun `should register get route with custom default content types`() { - val router = router { - defaultConsuming = setOf("text/plain") - defaultProducing = setOf("text/plain") - - POST("/some") { r: Request -> - ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") + val router = + router { + defaultConsuming = setOf("text/plain") + defaultProducing = setOf("text/plain") + + POST("/some") { r: Request -> + ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") + } } - } assertThat(router.routes).hasSize(1) with(router.routes.first().requestPredicate) { @@ -98,11 +101,12 @@ class RouterTest { @Test fun `should handle greedy path variables successfully`() { - val router = router { - POST("/some/{proxy+}") { r: Request -> - ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") + val router = + router { + POST("/some/{proxy+}") { r: Request -> + ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") + } } - } assertThat(router.routes).hasSize(1) with(router.routes.first().requestPredicate) { assertTrue(UriTemplate.from(pathPattern).matches("/some/sub/sub/sub/path")) @@ -111,11 +115,12 @@ class RouterTest { @Test fun `should not consume for a deletion route`() { - val router = router { - DELETE("/delete-me") { _: Request -> - ResponseEntity.ok(null) + val router = + router { + DELETE("/delete-me") { _: Request -> + ResponseEntity.ok(null) + } } - } with(router.routes.first().requestPredicate) { assertThat(consumes).isEqualTo(setOf()) } @@ -123,21 +128,24 @@ class RouterTest { @Test fun `request should contain ProxyRequestContext`() { - val claims = mapOf( - "foobar" to "foo" - ) - val context = APIGatewayProxyRequestEvent.ProxyRequestContext().apply { - authorizer = mapOf("claims" to claims) - } + val claims = + mapOf( + "foobar" to "foo", + ) + val context = + APIGatewayProxyRequestEvent.ProxyRequestContext().apply { + authorizer = mapOf("claims" to claims) + } - val request = Request( - APIGatewayProxyRequestEvent() - .withPath("/some-other") - .withHttpMethod("GET") - .withHeaders(mapOf("Accept" to "application/json")) - .withRequestContext(context), - Unit - ) + val request = + Request( + APIGatewayProxyRequestEvent() + .withPath("/some-other") + .withHttpMethod("GET") + .withHeaders(mapOf("Accept" to "application/json")) + .withRequestContext(context), + Unit, + ) assertThat(request.requestContext.authorizer!!["claims"]).isEqualTo(claims) } } diff --git a/router/src/test/kotlin/io/moia/router/UriTemplateTest.kt b/router/src/test/kotlin/io/moia/router/UriTemplateTest.kt index 742c24d5..7aafce45 100644 --- a/router/src/test/kotlin/io/moia/router/UriTemplateTest.kt +++ b/router/src/test/kotlin/io/moia/router/UriTemplateTest.kt @@ -24,16 +24,23 @@ import org.junit.jupiter.params.provider.MethodSource import java.util.UUID class UriTemplateTest { - @ParameterizedTest @MethodSource("matchTestParams") - fun `match template`(uriTemplate: String, matchTemplate: String, expectedResult: Boolean) { + fun `match template`( + uriTemplate: String, + matchTemplate: String, + expectedResult: Boolean, + ) { then(UriTemplate.from(uriTemplate).matches(matchTemplate)).isEqualTo(expectedResult) } @ParameterizedTest @MethodSource("extractTestParams") - fun `extract template`(uriTemplate: String, extractTemplate: String, expectedResult: Map) { + fun `extract template`( + uriTemplate: String, + extractTemplate: String, + expectedResult: Map, + ) { then(UriTemplate.from(uriTemplate).extract(extractTemplate)).isEqualTo(expectedResult) } @@ -48,62 +55,65 @@ class UriTemplateTest { companion object { @JvmStatic @Suppress("unused") - fun matchTestParams() = listOf( - Arguments.of("/some", "/some", true, "should match without parameter"), - Arguments.of("/some", "/some-other", false, "should not match simple"), - Arguments.of("/some/{id}", "/some/${UUID.randomUUID()}", true, "should match with parameter-1"), - Arguments.of("/some/{id}/other", "/some/${UUID.randomUUID()}/other", true, "should match with parameter-2"), - Arguments.of("/some/{id}", "/some-other/${UUID.randomUUID()}", false, "should not match with parameter-1"), - Arguments.of( - "/some/{id}/other", - "/some/${UUID.randomUUID()}/other-test", - false, - "should not match with parameter-2" - ), - Arguments.of("/some?a=1", "/some", true, "should match with query parameter 1"), - Arguments.of("/some?a=1&b=2", "/some", true, "should match with query parameter 2"), - Arguments.of( - "/some/{id}?a=1", - "/some/${UUID.randomUUID()}", - true, - "should match with path parameter and query parameter 1" - ), - Arguments.of( - "/some/{id}/other?a=1&b=2", - "/some/${UUID.randomUUID()}/other", - true, - "should match with path parameter and query parameter 2" - ), - Arguments.of( - "/some/{proxy+}", - "/some/sub/sub/sub/path", - true, - "should handle greedy path variables successfully" + fun matchTestParams() = + listOf( + Arguments.of("/some", "/some", true, "should match without parameter"), + Arguments.of("/some", "/some-other", false, "should not match simple"), + Arguments.of("/some/{id}", "/some/${UUID.randomUUID()}", true, "should match with parameter-1"), + Arguments.of("/some/{id}/other", "/some/${UUID.randomUUID()}/other", true, "should match with parameter-2"), + Arguments.of("/some/{id}", "/some-other/${UUID.randomUUID()}", false, "should not match with parameter-1"), + Arguments.of( + "/some/{id}/other", + "/some/${UUID.randomUUID()}/other-test", + false, + "should not match with parameter-2", + ), + Arguments.of("/some?a=1", "/some", true, "should match with query parameter 1"), + Arguments.of("/some?a=1&b=2", "/some", true, "should match with query parameter 2"), + Arguments.of( + "/some/{id}?a=1", + "/some/${UUID.randomUUID()}", + true, + "should match with path parameter and query parameter 1", + ), + Arguments.of( + "/some/{id}/other?a=1&b=2", + "/some/${UUID.randomUUID()}/other", + true, + "should match with path parameter and query parameter 2", + ), + Arguments.of( + "/some/{proxy+}", + "/some/sub/sub/sub/path", + true, + "should handle greedy path variables successfully", + ), ) - ) @JvmStatic @Suppress("unused") - fun extractTestParams() = listOf( - Arguments.of("/some", "/some", emptyMap(), "should extract parameters-1"), - Arguments.of( - "/some/{first}/other/{second}", - "/some/first-value/other/second-value", - mapOf("first" to "first-value", "second" to "second-value"), - "should extract parameters 2" + fun extractTestParams() = + listOf( + Arguments.of("/some", "/some", emptyMap(), "should extract parameters-1"), + Arguments.of( + "/some/{first}/other/{second}", + "/some/first-value/other/second-value", + mapOf("first" to "first-value", "second" to "second-value"), + "should extract parameters 2", + ), ) - ) @JvmStatic @Suppress("unused") - fun notAllowedGreedyPathTemplates() = listOf( - "/some/{proxy+}/and/{variable}/error", - "/{proxy+}/some/and/{variable}/error", - "/here/some/and/{proxy+}/{variable}", - "/here/some/and/{proxy+}/error", // FIXME: it should throw exception - "/here/some/and//good/good/{proxy+}/bad/bad/bad", // FIXME: it should throw exception - "/{proxy+}/{id}", - "/{proxy+}/whatever" - ) + fun notAllowedGreedyPathTemplates() = + listOf( + "/some/{proxy+}/and/{variable}/error", + "/{proxy+}/some/and/{variable}/error", + "/here/some/and/{proxy+}/{variable}", + "/here/some/and/{proxy+}/error", // FIXME: it should throw exception + "/here/some/and//good/good/{proxy+}/bad/bad/bad", // FIXME: it should throw exception + "/{proxy+}/{id}", + "/{proxy+}/whatever", + ) } }