Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/gradle/jvm-2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
blockvote committed Jun 3, 2024
2 parents ccd1f65 + e7d5668 commit e6b4fad
Show file tree
Hide file tree
Showing 35 changed files with 1,274 additions and 1,021 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand All @@ -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())

Expand All @@ -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<Unit> ->
ResponseEntity.ok(TestResponse("Hello"))
override val router =
Router.router {
GET("/tests") { _: Request<Unit> ->
ResponseEntity.ok(TestResponse("Hello"))
}
GET("/tests-not-documented") { _: Request<Unit> ->
ResponseEntity.ok(TestResponse("Hello"))
}
}
GET("/tests-not-documented") { _: Request<Unit> ->
ResponseEntity.ok(TestResponse("Hello"))
}
}
}

class TestInvalidRequestHandler : RequestHandler() {

data class TestResponseInvalid(val invalid: String)

override val router = Router.router {
GET("/tests") { _: Request<Unit> ->
ResponseEntity.ok(TestResponseInvalid("Hello"))
override val router =
Router.router {
GET("/tests") { _: Request<Unit> ->
ResponseEntity.ok(TestResponseInvalid("Hello"))
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}

Expand All @@ -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())
}
Expand All @@ -67,29 +71,32 @@ class ValidatingRequestRouterWrapperTest {
ValidatingRequestRouterWrapper(
delegate = OpenApiValidatorTest.TestRequestHandler(),
specUrlOrPayload = "openapi.yml",
additionalResponseValidationFunctions = listOf({ _, _ -> throw ResponseValidationFailedException() })
additionalResponseValidationFunctions = listOf({ _, _ -> throw ResponseValidationFailedException() }),
)
.handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk())
}
.isInstanceOf(ResponseValidationFailedException::class.java)
}

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<Unit> ->
ResponseEntity.ok("""{"name": "some"}""")
override val router =
router {
GET("/tests") { _: Request<Unit> ->
ResponseEntity.ok("""{"name": "some"}""")
}
}
}
}

private class InvalidTestRequestHandler : RequestHandler() {
override val router = router {
GET("/tests") { _: Request<Unit> ->
ResponseEntity.notFound(Unit)
override val router =
router {
GET("/tests") { _: Request<Unit> ->
ResponseEntity.notFound(Unit)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ object ProtoBufUtils {

fun removeWrapperObjects(json: String): String {
return removeWrapperObjects(
jacksonObjectMapper().readTree(json)
jacksonObjectMapper().readTree(json),
).toString()
}

Expand All @@ -38,7 +38,7 @@ object ProtoBufUtils {
if (entry.value.size() > 0) {
result.replace(
entry.key,
removeWrapperObjects(entry.value)
removeWrapperObjects(entry.value),
)
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> createResponse(
contentType: MediaType,
response: ResponseEntity<T>
response: ResponseEntity<T>,
): APIGatewayProxyResponseEvent {
return super.createResponse(contentType, response).withIsBase64Encoded(true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit e6b4fad

Please sign in to comment.