Skip to content

Commit 5e94740

Browse files
committed
add openapi request validator
1 parent 4357833 commit 5e94740

File tree

5 files changed

+173
-1
lines changed

5 files changed

+173
-1
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
repositories {
2+
mavenCentral()
3+
}
4+
5+
dependencies {
6+
compile(kotlin("stdlib-jdk8"))
7+
compile(kotlin("reflect"))
8+
9+
compile("com.atlassian.oai:swagger-request-validator-core:2.2.2")
10+
compile(project(":router"))
11+
12+
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.4.0")
13+
testImplementation("org.assertj:assertj-core:3.11.1")
14+
testImplementation("io.mockk:mockk:1.8.13.kotlin13")
15+
testImplementation("org.slf4j:slf4j-simple:1.7.26")
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.github.mduesterhoeft.router.openapi
2+
3+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
4+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
5+
import com.atlassian.oai.validator.OpenApiInteractionValidator
6+
import com.atlassian.oai.validator.model.Request
7+
import com.atlassian.oai.validator.model.Response
8+
import com.atlassian.oai.validator.model.SimpleRequest
9+
import com.atlassian.oai.validator.model.SimpleResponse
10+
import com.atlassian.oai.validator.report.ValidationReport
11+
import org.slf4j.LoggerFactory
12+
13+
class OpenApiValidator(val specUrlOrPayload: String) {
14+
val validator = OpenApiInteractionValidator.createFor(specUrlOrPayload).build()
15+
16+
fun validate(request: APIGatewayProxyRequestEvent, response: APIGatewayProxyResponseEvent): ValidationReport {
17+
return validator.validate(request.toRequest(), response.toResponse())
18+
.also { if (it.hasErrors()) log.error("error validating request and response against $specUrlOrPayload - $it") }
19+
}
20+
21+
fun assertValid(request: APIGatewayProxyRequestEvent, response: APIGatewayProxyResponseEvent) {
22+
return validate(request, response).let { if (it.hasErrors()) throw ApiInteractionInvalid(specUrlOrPayload, request, response, it) }
23+
}
24+
25+
class ApiInteractionInvalid(val spec: String, val request: APIGatewayProxyRequestEvent, val response: APIGatewayProxyResponseEvent, val validationReport: ValidationReport) :
26+
RuntimeException("Error validating request and response against $spec - $validationReport")
27+
28+
private fun APIGatewayProxyRequestEvent.toRequest(): Request {
29+
val builder = when (httpMethod.toLowerCase()) {
30+
"get" -> SimpleRequest.Builder.get(path)
31+
"post" -> SimpleRequest.Builder.post(path)
32+
"put" -> SimpleRequest.Builder.put(path)
33+
"patch" -> SimpleRequest.Builder.patch(path)
34+
"delete" -> SimpleRequest.Builder.delete(path)
35+
"options" -> SimpleRequest.Builder.options(path)
36+
"head" -> SimpleRequest.Builder.head(path)
37+
else -> throw IllegalArgumentException("Unsupported method $httpMethod")
38+
}
39+
headers?.forEach { builder.withHeader(it.key, it.value) }
40+
queryStringParameters?.forEach { builder.withQueryParam(it.key, it.value) }
41+
builder.withBody(body)
42+
return builder.build()
43+
}
44+
45+
private fun APIGatewayProxyResponseEvent.toResponse(): Response {
46+
return SimpleResponse.Builder
47+
.status(statusCode)
48+
.withBody(body)
49+
.also { headers.forEach { h -> it.withHeader(h.key, h.value) } }
50+
.build()
51+
}
52+
53+
companion object {
54+
val log = LoggerFactory.getLogger(OpenApiValidator::class.java)
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.github.mduesterhoeft.router.openapi
2+
3+
import com.github.mduesterhoeft.router.GET
4+
import com.github.mduesterhoeft.router.Request
5+
import com.github.mduesterhoeft.router.RequestHandler
6+
import com.github.mduesterhoeft.router.ResponseEntity
7+
import com.github.mduesterhoeft.router.Router
8+
import io.mockk.mockk
9+
import org.assertj.core.api.BDDAssertions.thenThrownBy
10+
import org.junit.jupiter.api.Test
11+
12+
class OpenApiValidatorTest {
13+
14+
val testHandler = TestRequestHandler()
15+
16+
val validator = OpenApiValidator("openapi.yml")
17+
18+
@Test
19+
fun `should handle and validate request`() {
20+
val request = GET("/tests")
21+
.withHeaders(mapOf("Accept" to "application/json"))
22+
23+
val response = testHandler.handleRequest(request, mockk())
24+
25+
validator.assertValid(request, response)
26+
}
27+
28+
@Test
29+
fun `should fail on undocumented request`() {
30+
val request = GET("/tests-not-documented")
31+
.withHeaders(mapOf("Accept" to "application/json"))
32+
33+
val response = testHandler.handleRequest(request, mockk())
34+
35+
thenThrownBy { validator.assertValid(request, response) }.isInstanceOf(OpenApiValidator.ApiInteractionInvalid::class.java)
36+
}
37+
38+
@Test
39+
fun `should fail on invalid schema`() {
40+
val request = GET("/tests")
41+
.withHeaders(mapOf("Accept" to "application/json"))
42+
43+
val response = TestInvalidRequestHandler().handleRequest(request, mockk())
44+
45+
thenThrownBy { validator.assertValid(request, response) }.isInstanceOf(OpenApiValidator.ApiInteractionInvalid::class.java)
46+
}
47+
48+
class TestRequestHandler : RequestHandler() {
49+
50+
data class TestResponse(val name: String)
51+
52+
override val router = Router.router {
53+
GET("/tests") { _: Request<Unit> ->
54+
ResponseEntity.ok(TestResponse("Hello"))
55+
}
56+
GET("/tests-not-documented") { _: Request<Unit> ->
57+
ResponseEntity.ok(TestResponse("Hello"))
58+
}
59+
}
60+
}
61+
62+
class TestInvalidRequestHandler : RequestHandler() {
63+
64+
data class TestResponseInvalid(val invalid: String)
65+
66+
override val router = Router.router {
67+
GET("/tests") { _: Request<Unit> ->
68+
ResponseEntity.ok(TestResponseInvalid("Hello"))
69+
}
70+
}
71+
}
72+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Test
5+
paths:
6+
/tests:
7+
get:
8+
summary: List all test records
9+
operationId: get-tests
10+
tags:
11+
- tests
12+
responses:
13+
'200':
14+
description: All tests
15+
content:
16+
application/json:
17+
schema:
18+
$ref: "#/components/schemas/Test"
19+
components:
20+
schemas:
21+
Test:
22+
required:
23+
- name
24+
properties:
25+
name:
26+
type: string
27+
description: unique id of the service area

settings.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
rootProject.name = 'lambda-kotlin-request-router'
22
include 'router'
3-
include 'router-protobuf'
3+
include 'router-protobuf'
4+
include 'router-openapi-request-validator'

0 commit comments

Comments
 (0)