Test openapi schema of Register

Fix some routes
Improve Schema Validation
This commit is contained in:
2021-03-16 00:53:10 +01:00
parent 235de4e5ff
commit 0cf1aea9bf
15 changed files with 179 additions and 73 deletions

View File

@@ -30,7 +30,7 @@ services:
build: build:
context: docker/postgresql context: docker/postgresql
ports: ports:
- 5433:5432 - 15432:5432
environment: environment:
POSTGRES_PASSWORD: ${DB_NAME} POSTGRES_PASSWORD: ${DB_NAME}
POSTGRES_USER: ${DB_USER} POSTGRES_USER: ${DB_USER}

View File

@@ -0,0 +1,10 @@
package fr.dcproject.common
interface BitMaskI {
val bit: Long
infix operator fun contains(which: BitMaskI): Boolean = bit and which.bit == which.bit
infix operator fun plus(mask: BitMaskI): BitMaskI = BitMask(mask.bit and this.bit)
}
class BitMask(override val bit: Long) : BitMaskI

View File

@@ -11,10 +11,12 @@ import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRepository import fr.dcproject.component.citizen.database.CitizenRepository
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.BadRequestException import io.ktor.features.BadRequestException
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
import io.ktor.locations.post import io.ktor.locations.post
import io.ktor.request.accept
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.Route import io.ktor.routing.Route
@@ -61,8 +63,17 @@ object Register {
post<RegisterRequest> { post<RegisterRequest> {
try { try {
val citizen = call.receiveOrBadRequest<Input>().toCitizen() val citizen = call.receiveOrBadRequest<Input>().toCitizen()
val createdCitizen = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request") citizenRepo.insertWithUser(citizen)?.user?.makeToken()?.let { token ->
call.respondText(createdCitizen.makeToken()) if (call.request.accept() == ContentType.Application.Json.toString()) {
call.respond(
object {
val token: String = token
}
)
} else {
call.respondText(token)
}
} ?: throw BadRequestException("Bad request")
} catch (e: MissingKotlinParameterException) { } catch (e: MissingKotlinParameterException) {
call.respond(HttpStatusCode.BadRequest) call.respond(HttpStatusCode.BadRequest)
} }

View File

@@ -256,6 +256,82 @@ paths:
type: string type: string
example: example:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBdXRoZW50aWNhdGlvbiIsImlzcyI6ImRjLXByb2plY3QuZnIiLCJpZCI6ImQ1NDRhNmE4LWJhYjgtNDU2MC05NWIxLThhZjAyMDNkOTEwNCIsImV4cCI6MTU2NzA3Mzc0Mn0.0VTetv8fZFjVgpJ-bwJpidGNHJUOmgj8vuZcZXzwnLa7TtFwcXWvh3bDPYHqB66nmOfXyM57XnHDbmRwtipCag' 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBdXRoZW50aWNhdGlvbiIsImlzcyI6ImRjLXByb2plY3QuZnIiLCJpZCI6ImQ1NDRhNmE4LWJhYjgtNDU2MC05NWIxLThhZjAyMDNkOTEwNCIsImV4cCI6MTU2NzA3Mzc0Mn0.0VTetv8fZFjVgpJ-bwJpidGNHJUOmgj8vuZcZXzwnLa7TtFwcXWvh3bDPYHqB66nmOfXyM57XnHDbmRwtipCag'
/register:
post:
summary: Create account
tags:
- authentification
operationId: register
requestBody:
content:
application/json:
schema:
type: object
required:
- name
- birthday
- email
- user
properties:
name:
type: object
required:
- firstName
- lastName
properties:
firstName:
type: string
example:
john
lastName:
type: string
example:
Doe
birthday:
type: string
format: 'date'
example: '1984-12-25'
email:
type: string
format: email
example: my.email@dc-project.fr
user:
type: object
required:
- username
- password
properties:
username:
type: string
example:
john-doe
password:
type: string
example:
azerty
format: password
responses:
200:
description: User created and JWT returned
content:
text/plain:
example:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBdXRoZW50aWNhdGlvbiIsImlzcyI6ImRjLXByb2plY3QuZnIiLCJpZCI6ImQ1NDRhNmE4LWJhYjgtNDU2MC05NWIxLThhZjAyMDNkOTEwNCIsImV4cCI6MTU2NzA3Mzc0Mn0.0VTetv8fZFjVgpJ-bwJpidGNHJUOmgj8vuZcZXzwnLa7TtFwcXWvh3bDPYHqB66nmOfXyM57XnHDbmRwtipCag'
application/json:
schema:
properties:
token:
type: string
example:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBdXRoZW50aWNhdGlvbiIsImlzcyI6ImRjLXByb2plY3QuZnIiLCJpZCI6ImQ1NDRhNmE4LWJhYjgtNDU2MC05NWIxLThhZjAyMDNkOTEwNCIsImV4cCI6MTU2NzA3Mzc0Mn0.0VTetv8fZFjVgpJ-bwJpidGNHJUOmgj8vuZcZXzwnLa7TtFwcXWvh3bDPYHqB66nmOfXyM57XnHDbmRwtipCag'
400:
description: Bad request
content:
application/json:
schema:
description: sdf
components: components:
parameters: parameters:
page: page:

View File

@@ -1,5 +1,6 @@
package integration package integration
import integration.steps.`when`.Validate
import integration.steps.then.`And have property` import integration.steps.then.`And have property`
import integration.steps.then.`And the response should not be null` import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
@@ -67,8 +68,8 @@ class `Citizen routes` : BaseTest() {
`authenticated as`("Georges", "Charpak") `authenticated as`("Georges", "Charpak")
`with body`(""" `with body`("""
{ {
"old_password": "azerty", "oldPassword": "azerty",
"new_password": "qwerty" "newPassword": "qwerty"
} }
""") """)
} `Then the response should be` Created } `Then the response should be` Created
@@ -79,7 +80,7 @@ class `Citizen routes` : BaseTest() {
fun `I cannot change my password if request is bad formated`() { fun `I cannot change my password if request is bad formated`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Louis", "Breguet", id = "6cf2a19d-d15d-4ee5-b2a9-907afd26b525") `Given I have citizen`("Louis", "Breguet", id = "6cf2a19d-d15d-4ee5-b2a9-907afd26b525")
`When I send a PUT request`("/citizens/6cf2a19d-d15d-4ee5-b2a9-907afd26b525/password/change") { `When I send a PUT request`("/citizens/6cf2a19d-d15d-4ee5-b2a9-907afd26b525/password/change", Validate.RESPONSE_BODY) {
`authenticated as`("Louis", "Breguet") `authenticated as`("Louis", "Breguet")
`with body`(""" `with body`("""
{ {

View File

@@ -84,9 +84,9 @@ class `Comment articles routes` : BaseTest() {
`When I send a GET request`("/citizens/292a20cc-4a60-489e-9866-a95d38ffaf47/comments/articles") { `When I send a GET request`("/citizens/292a20cc-4a60-489e-9866-a95d38ffaf47/comments/articles") {
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.current_page", 1) `And the response should contain`("$.currentPage", 1)
`And the response should contain`("$.limit", 50) `And the response should contain`("$.limit", 50)
`And the response should contain`("$.result[0]created_by.id", "292a20cc-4a60-489e-9866-a95d38ffaf47") `And the response should contain`("$.result[0]createdBy.id", "292a20cc-4a60-489e-9866-a95d38ffaf47")
} }
} }
} }

View File

@@ -15,7 +15,6 @@ import integration.steps.given.`Given I have constitution`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import io.ktor.server.testing.setBody
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -51,9 +50,9 @@ class `Comment constitutions routes` : BaseTest() {
`When I send a GET request`("/citizens/46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5/comments/constitutions") { `When I send a GET request`("/citizens/46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5/comments/constitutions") {
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.current_page", 1) `And the response should contain`("$.currentPage", 1)
`And the response should contain`("$.limit", 50) `And the response should contain`("$.limit", 50)
`And the response should contain`("$.result[0].created_by.id", "46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5") `And the response should contain`("$.result[0].createdBy.id", "46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5")
`And the response should contain`("$.result[0].target.id", "34ddd50a-da00-4a90-a869-08baa2a121be") `And the response should contain`("$.result[0].target.id", "34ddd50a-da00-4a90-a869-08baa2a121be")
`And the response should contain list`("$.result[*]", 1, 1) `And the response should contain list`("$.result[*]", 1, 1)
} }

View File

@@ -47,7 +47,7 @@ class `Follow articles routes` : BaseTest() {
`authenticated as`("Johannes", "Kepler") `authenticated as`("Johannes", "Kepler")
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.current_page", 1) `And the response should contain`("$.currentPage", 1)
`And the response should contain`("$.limit", 50) `And the response should contain`("$.limit", 50)
} }
} }

View File

@@ -47,7 +47,7 @@ class `Follow constitutions routes` : BaseTest() {
`authenticated as`("André-Marie", "Ampère") `authenticated as`("André-Marie", "Ampère")
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.current_page", 1) `And the response should contain`("$.currentPage", 1)
`And the response should contain`("$.limit", 50) `And the response should contain`("$.limit", 50)
} }
} }

View File

@@ -1,12 +1,15 @@
package integration package integration
import integration.steps.then.`Then the response should be` import integration.steps.`when`.Validate
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import io.ktor.http.HttpStatusCode import integration.steps.then.`And the response should be null`
import org.amshove.kluent.`should be null` import integration.steps.then.`And the response should contain pattern`
import org.amshove.kluent.`should contain` import integration.steps.then.`And the response should not be null`
import org.amshove.kluent.`should not be null` import integration.steps.then.`Then the response should be`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -21,7 +24,7 @@ class `Register routes` : BaseTest() {
`When I send a POST request`("/register") { `When I send a POST request`("/register") {
`with body`(""" `with body`("""
{ {
"name": {"first_name":"George", "last_name":"MICHEL"}, "name": {"firstName":"George", "lastName":"MICHEL"},
"birthday": "2001-01-01", "birthday": "2001-01-01",
"user":{ "user":{
"username": "george-junior", "username": "george-junior",
@@ -30,10 +33,9 @@ class `Register routes` : BaseTest() {
"email": "george-junior@gmail.com" "email": "george-junior@gmail.com"
} }
""") """)
}.`Then the response should be`(HttpStatusCode.OK) { } `Then the response should be` OK and {
content `And the response should not be null`()
.`should not be null`() `And the response should contain pattern`("$.token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.")
.`should contain`("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.")
} }
} }
} }
@@ -41,19 +43,19 @@ class `Register routes` : BaseTest() {
@Test @Test
fun `I cannot register if no username was sent`() { fun `I cannot register if no username was sent`() {
withIntegrationApplication { withIntegrationApplication {
`When I send a POST request`("/register") { `When I send a POST request`("/register", Validate.RESPONSE_BODY) {
`with body`(""" `with body`("""
{ {
"name": {"first_name":"George2", "last_name":"MICHEL2"}, "name": {"firstName":"George2", "lastName":"MICHEL2"},
"birthday": "2001-01-01", "birthday": "2001-01-01",
"user":{ "user":{
"username": "",
"password": "" "password": ""
} },
"email": "george-junior@gmail.com"
} }
""") """)
}.`Then the response should be`(HttpStatusCode.BadRequest) { } `Then the response should be` BadRequest and {
content.`should be null`() `And the response should be null`()
} }
} }
} }

View File

@@ -65,7 +65,7 @@ class `Vote routes` : BaseTest() {
`When I send a GET request`("/citizens/c044823d-e778-4256-9016-b1334bf933d3/votes/articles") { `When I send a GET request`("/citizens/c044823d-e778-4256-9016-b1334bf933d3/votes/articles") {
`authenticated as`("Carl", "Gauss") `authenticated as`("Carl", "Gauss")
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain`("$.current_page", 1) `And the response should contain`("$.currentPage", 1)
`And the response should contain`("$.limit", 50) `And the response should contain`("$.limit", 50)
`And the response should contain`("$.total", 1) `And the response should contain`("$.total", 1)
`And the response should contain`("$.result[0].note", 1) `And the response should contain`("$.result[0].note", 1)

View File

@@ -50,11 +50,11 @@ class `Workgroup routes` : BaseTest() {
`And the response should contain`("$.id", "ab469134-bf14-4856-b093-ae1aa990f977") `And the response should contain`("$.id", "ab469134-bf14-4856-b093-ae1aa990f977")
`And the response should contain`("$.name", "Les Mousquets") `And the response should contain`("$.name", "Les Mousquets")
`And the response should contain`( `And the response should contain`(
"$.members[*].citizen.name[?(@.first_name=='Stephen')].first_name", "$.members[*].citizen.name[?(@.firstName=='Stephen')].firstName",
"Stephen" "Stephen"
) )
`And the response should contain`( `And the response should contain`(
"$.members[*].citizen.name[?(@.first_name=='Sadi')].first_name", "$.members[*].citizen.name[?(@.firstName=='Sadi')].firstName",
"Sadi" "Sadi"
) )
} }

View File

@@ -50,17 +50,26 @@ fun TestApplicationResponse.operation(route: String? = null, callback: Operation
} }
} }
fun TestApplicationResponse.`And the schema must be valid`(route: String? = null, contentType: ContentType? = ContentType.Application.Json) { fun TestApplicationResponse.`And the schema response body must be valid`(contentType: ContentType? = ContentType.Application.Json) {
operation(route) { api, uri -> operation { api, uri ->
/* Validate Response */ /* Validate Response */
this.apply { this.apply {
val status = call.response.status() val status = call.response.status()
val responseContent: JsonNode = if (content != null)
ObjectMapper().readTree(content)
else TextNode("")
getResponse(status?.value?.toString() ?: error("HttpStatus not found")) getResponse(status?.value?.toString() ?: error("HttpStatus not found"))
?.getContentMediaType(contentType.toString()) ?.getContentMediaType(contentType.toString())
?.schema ?.schema
?.validate(api, ObjectMapper().readTree(content)) ?.validate(api, responseContent)
?: fail("""No Status "${status.value}" found with media type "$contentType" for "$this $uri".""") ?: fail("""No Status "${status.value}" found with media type "$contentType" for "$this $uri".""")
} }
}
}
fun TestApplicationResponse.`And the schema parameters must be valid`() {
operation { api, uri ->
/* Validate Request URL */ /* Validate Request URL */
this.apply { this.apply {
Url(call.request.uri).parameters.forEach { parameter: String, values: List<String> -> Url(call.request.uri).parameters.forEach { parameter: String, values: List<String> ->

View File

@@ -1,7 +1,9 @@
package integration.steps.`when` package integration.steps.`when`
import integration.steps.then.`And the schema must be valid` import fr.dcproject.common.BitMaskI
import integration.steps.then.`And the schema parameters must be valid`
import integration.steps.then.`And the schema request body must be valid` import integration.steps.then.`And the schema request body must be valid`
import integration.steps.then.`And the schema response body must be valid`
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
@@ -11,7 +13,31 @@ import io.ktor.server.testing.TestApplicationEngine
import io.ktor.server.testing.TestApplicationRequest import io.ktor.server.testing.TestApplicationRequest
import io.ktor.server.testing.setBody import io.ktor.server.testing.setBody
fun TestApplicationEngine.`When I send a GET request`(uri: String? = null, validate: Boolean = true, setup: (TestApplicationRequest.() -> Unit)? = null): TestApplicationCall { enum class Validate(override val bit: Long) : BitMaskI {
REQUEST_BODY(1),
REQUEST_PARAM(2),
REQUEST(3),
RESPONSE_BODY(4),
ALL(7);
}
fun TestApplicationCall.valid(validate: Validate): TestApplicationCall {
if (Validate.RESPONSE_BODY in validate) {
response.`And the schema response body must be valid`()
}
if (Validate.REQUEST_PARAM in validate) {
response.`And the schema parameters must be valid`()
}
if (Validate.REQUEST_BODY in validate) {
requestBody?.let { body ->
response.`And the schema request body must be valid`(body)
}
}
return this
}
fun TestApplicationEngine.`When I send a GET request`(uri: String? = null, validate: Validate = Validate.ALL, setup: (TestApplicationRequest.() -> Unit)? = null): TestApplicationCall {
return handleRequest(true) { return handleRequest(true) {
method = HttpMethod.Get method = HttpMethod.Get
if (uri != null) { if (uri != null) {
@@ -19,17 +45,10 @@ fun TestApplicationEngine.`When I send a GET request`(uri: String? = null, valid
} }
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString()) addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setup?.let { it() } setup?.let { it() }
}.apply { }.valid(validate)
if (validate) {
response.`And the schema must be valid`()
requestBody?.let { body ->
response.`And the schema request body must be valid`(body)
}
}
}
} }
fun TestApplicationEngine.`When I send a POST request`(uri: String? = null, validate: Boolean = true, setup: (TestApplicationRequest.() -> Unit)? = null): TestApplicationCall { fun TestApplicationEngine.`When I send a POST request`(uri: String? = null, validate: Validate = Validate.ALL, setup: (TestApplicationRequest.() -> Unit)? = null): TestApplicationCall {
return handleRequest(true) { return handleRequest(true) {
method = HttpMethod.Post method = HttpMethod.Post
if (uri != null) { if (uri != null) {
@@ -38,17 +57,10 @@ fun TestApplicationEngine.`When I send a POST request`(uri: String? = null, vali
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString()) addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
addHeader(HttpHeaders.Accept, ContentType.Application.Json.toString()) addHeader(HttpHeaders.Accept, ContentType.Application.Json.toString())
setup?.let { it() } setup?.let { it() }
}.apply { }.valid(validate)
if (validate) {
response.`And the schema must be valid`()
requestBody?.let { body ->
response.`And the schema request body must be valid`(body)
}
}
}
} }
fun TestApplicationEngine.`When I send a PUT request`(uri: String? = null, validate: Boolean = true, setup: (TestApplicationRequest.() -> Unit)? = null): TestApplicationCall { fun TestApplicationEngine.`When I send a PUT request`(uri: String? = null, validate: Validate = Validate.ALL, setup: (TestApplicationRequest.() -> Unit)? = null): TestApplicationCall {
return handleRequest(true) { return handleRequest(true) {
method = HttpMethod.Put method = HttpMethod.Put
if (uri != null) { if (uri != null) {
@@ -56,17 +68,10 @@ fun TestApplicationEngine.`When I send a PUT request`(uri: String? = null, valid
} }
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString()) addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setup?.let { it() } setup?.let { it() }
}.apply { }.valid(validate)
if (validate) {
response.`And the schema must be valid`()
requestBody?.let { body ->
response.`And the schema request body must be valid`(body)
}
}
}
} }
fun TestApplicationEngine.`When I send a DELETE request`(uri: String? = null, validate: Boolean = true, setup: (TestApplicationRequest.() -> String?)? = null): TestApplicationCall { fun TestApplicationEngine.`When I send a DELETE request`(uri: String? = null, validate: Validate = Validate.ALL, setup: (TestApplicationRequest.() -> String?)? = null): TestApplicationCall {
return handleRequest(true) { return handleRequest(true) {
method = HttpMethod.Delete method = HttpMethod.Delete
if (uri != null) { if (uri != null) {
@@ -76,14 +81,7 @@ fun TestApplicationEngine.`When I send a DELETE request`(uri: String? = null, va
setup?.let { it() }?.let { setup?.let { it() }?.let {
setBody(it.trimIndent()) setBody(it.trimIndent())
} }
}.apply { }.valid(validate)
if (validate) {
response.`And the schema must be valid`()
requestBody?.let { body ->
response.`And the schema request body must be valid`(body)
}
}
}
} }
private val requestBodies: MutableMap<ApplicationCall, String> = mutableMapOf() private val requestBodies: MutableMap<ApplicationCall, String> = mutableMapOf()

View File

@@ -17,7 +17,7 @@ db {
database = test database = test
username = test username = test
password = test password = test
port = 5433 port = 15432
} }
redis { redis {