From 4762275b5b14f9a1c82dd7741872e8b51dd5f78f Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Mon, 8 Mar 2021 23:13:48 +0100 Subject: [PATCH] Test openapi schema response of FindArticles --- build.gradle.kts | 2 + .../fr/dcproject/common/utils/Resources.kt | 10 +- src/main/resources/openapi2.yaml | 188 ++++++++++++++++++ src/test/kotlin/integration/Article routes.kt | 2 + .../kotlin/integration/steps/then/schema.kt | 45 +++++ 5 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/openapi2.yaml create mode 100644 src/test/kotlin/integration/steps/then/schema.kt diff --git a/build.gradle.kts b/build.gradle.kts index ea245c1..9862d9d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -347,4 +347,6 @@ dependencies { testImplementation("io.mockk:mockk-agent-jvm:1.10.6") testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") testImplementation("com.thedeanda:lorem:2.1") + testImplementation("org.openapi4j:openapi-operation-validator:1.0.6") + testImplementation("org.openapi4j:openapi-parser:1.0.6") } diff --git a/src/main/kotlin/fr/dcproject/common/utils/Resources.kt b/src/main/kotlin/fr/dcproject/common/utils/Resources.kt index 0a3de93..9d1e4df 100644 --- a/src/main/kotlin/fr/dcproject/common/utils/Resources.kt +++ b/src/main/kotlin/fr/dcproject/common/utils/Resources.kt @@ -1,7 +1,15 @@ package fr.dcproject.common.utils +import java.net.URL + fun String.readResource(callback: (String) -> Unit = {}): String { - val content = callback::class.java.getResource(this).readText() + val content = callback::class.java.getResource(this)?.readText() ?: error("File not found") + callback(content) + return content +} + +fun String.getResource(callback: (URL) -> Unit = {}): URL { + val content = callback::class.java.getResource(this) ?: error("File not found") callback(content) return content } diff --git a/src/main/resources/openapi2.yaml b/src/main/resources/openapi2.yaml new file mode 100644 index 0000000..029fd5f --- /dev/null +++ b/src/main/resources/openapi2.yaml @@ -0,0 +1,188 @@ +openapi: 3.0.2 +info: + version: '0.1' + title: 'DC Project' + description: 'A free comunity program for create constitution' + +paths: + /articles: + get: + summary: Get all articles + tags: + - article + operationId: getArticles + parameters: + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/articleSort' + - $ref: '#/components/parameters/direction' + - $ref: '#/components/parameters/search' + - $ref: '#/components/parameters/createdBy' + - name: workgroup + in: query + description: ID of workgroup + example: 82a0e60a-bb55-dbc0-1c3d-0a804df2b5df + required: false + schema: + type: string + format: uuid + responses: + 200: + description: The Article objects + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Paginated' + - type: object + properties: + result: + type: array + items: + properties: + id: + type: string + format: uuid + title: + type: string + created_by: + type: object + properties: + id: + type: string + format: uuid + name: + type: object + properties: + first_name: + type: string + last_name: + type: string + email: + type: string + workgroup: + type: object + nullable: true + properties: + id: + type: string + format: uuid + name: + type: string + draft: + type: boolean + +components: + parameters: + page: + name: page + in: query + description: The current page + example: 1 + required: false + schema: + default: 1 + type: integer + minimum: 1 + limit: + name: limit + in: query + description: The number of object per page + example: 50 + required: false + schema: + default: 50 + type: integer + minimum: 1 + maximum: 50 + sort: + name: sort + in: query + description: The sort field name + example: first_name + required: false + schema: + type: string + articleSort: + name: sort + in: query + description: The sort field name + example: createdAt + required: false + schema: + type: string + enum: + - title + - createdAt + - vote + - popularity + direction: + name: direction + in: query + description: The sort direction + example: asc + required: false + schema: + type: string + default: asc + enum: [asc, desc] + search: + name: search + in: query + description: A text to seach + example: content50 + required: false + schema: + type: string + createdBy: + name: createdBy + in: query + description: filter by Author + example: 4d673bfa-eaef-4290-b52f-85a9c8a7eba5 + required: false + schema: + type: string + format: uuid + schemas: + UUID: + type: string + pattern: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' + description: UUID + format: uuid + example: + e74be8e4-6823-47c4-bd1b-789725b2fa8e + UuidEntity: + properties: + id: + $ref: '#/components/schemas/UUID' + Paginated: + properties: + result: + type: array + items: + $ref: '#/components/schemas/UuidEntity' + count: + type: integer + minimum: 0 + example: 1 + currentPage: + type: integer + minimum: 0 + example: 1 + limit: + type: integer + minimum: 0 + example: 50 + offset: + type: integer + minimum: 0 + example: 1 + total: + type: integer + minimum: 0 + example: 1 +servers: + - description: localhost + url: http://localhost:8080 + - description: production + url: http://dc-project.fr \ No newline at end of file diff --git a/src/test/kotlin/integration/Article routes.kt b/src/test/kotlin/integration/Article routes.kt index 66f61f6..b4e0325 100644 --- a/src/test/kotlin/integration/Article routes.kt +++ b/src/test/kotlin/integration/Article routes.kt @@ -14,6 +14,7 @@ import integration.steps.given.`Given I have articles` import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have workgroup` import integration.steps.given.`authenticated as` +import integration.steps.then.`And schema must be valid` import integration.steps.then.`And the response should contain list` import integration.steps.then.`And the response should not contain` import io.ktor.http.HttpStatusCode.Companion.OK @@ -36,6 +37,7 @@ class `Article routes` : BaseTest() { `And the response should contain pattern`("$.result[2].created_by.name.first_name", "firstName.+") `And the response should not contain`("$.result[3]") `And the response should contain list`("$.result", 3, 3) + `And schema must be valid`() } } } diff --git a/src/test/kotlin/integration/steps/then/schema.kt b/src/test/kotlin/integration/steps/then/schema.kt new file mode 100644 index 0000000..0ccf3ac --- /dev/null +++ b/src/test/kotlin/integration/steps/then/schema.kt @@ -0,0 +1,45 @@ +package integration.steps.then + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import fr.dcproject.common.utils.getResource +import io.ktor.request.contentType +import io.ktor.request.httpMethod +import io.ktor.request.uri +import io.ktor.server.testing.TestApplicationResponse +import org.openapi4j.core.model.v3.OAI3 +import org.openapi4j.parser.OpenApi3Parser +import org.openapi4j.parser.model.v3.OpenApi3 +import org.openapi4j.parser.model.v3.Schema +import org.openapi4j.schema.validator.ValidationContext +import org.openapi4j.schema.validator.ValidationData +import org.openapi4j.schema.validator.v3.SchemaValidator +import java.io.File +import kotlin.test.assertTrue + +fun TestApplicationResponse.`And schema must be valid`() { + // Parse without validation, setting to true is strongly recommended for further data validation. + val api: OpenApi3 = OpenApi3Parser().parse(File("/openapi2.yaml".getResource().toURI()), true) + + val mediaType = this.call.request.contentType() + val operation = this.call.request.httpMethod + val uri = this.call.request.uri + val status = this.call.response.status() + + val schema: Schema = api + .getPath(uri) + .getOperation(operation.value.toLowerCase()) + .getResponse(status?.value?.toString() ?: error("HttpStatus not found")) + .getContentMediaType(mediaType.toString()) + .schema + + val validationContext: ValidationContext = ValidationContext(api.context) + val jsonNode: JsonNode = schema.toNode() + val schemaValidator = SchemaValidator(validationContext, "", jsonNode) + + val mapper = ObjectMapper() + val results = ValidationData() + schemaValidator.validate(mapper.readTree(content), results) + + assertTrue(results.isValid, results.results().toString()) +}