diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 4e0a840..e66057b 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -30,7 +30,7 @@ services: build: context: docker/postgresql ports: - - 5433:5432 + - 15432:5432 environment: POSTGRES_PASSWORD: ${DB_NAME} POSTGRES_USER: ${DB_USER} diff --git a/src/main/kotlin/fr/dcproject/common/BitMaskEnum.kt b/src/main/kotlin/fr/dcproject/common/BitMaskEnum.kt new file mode 100644 index 0000000..9bc6c48 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/common/BitMaskEnum.kt @@ -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 diff --git a/src/main/kotlin/fr/dcproject/component/auth/routes/Register.kt b/src/main/kotlin/fr/dcproject/component/auth/routes/Register.kt index b9bba46..08d1d67 100644 --- a/src/main/kotlin/fr/dcproject/component/auth/routes/Register.kt +++ b/src/main/kotlin/fr/dcproject/component/auth/routes/Register.kt @@ -11,10 +11,12 @@ import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenRepository import io.ktor.application.call import io.ktor.features.BadRequestException +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location import io.ktor.locations.post +import io.ktor.request.accept import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.Route @@ -61,8 +63,17 @@ object Register { post { try { val citizen = call.receiveOrBadRequest().toCitizen() - val createdCitizen = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request") - call.respondText(createdCitizen.makeToken()) + citizenRepo.insertWithUser(citizen)?.user?.makeToken()?.let { token -> + 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) { call.respond(HttpStatusCode.BadRequest) } diff --git a/src/main/resources/openapi2.yaml b/src/main/resources/openapi2.yaml index 1499980..6684854 100644 --- a/src/main/resources/openapi2.yaml +++ b/src/main/resources/openapi2.yaml @@ -256,6 +256,82 @@ paths: type: string example: '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: parameters: page: diff --git a/src/test/kotlin/integration/Citizen routes.kt b/src/test/kotlin/integration/Citizen routes.kt index 9ad6e7e..66d221c 100644 --- a/src/test/kotlin/integration/Citizen routes.kt +++ b/src/test/kotlin/integration/Citizen routes.kt @@ -1,5 +1,6 @@ package integration +import integration.steps.`when`.Validate import integration.steps.then.`And have property` import integration.steps.then.`And the response should not be null` import integration.steps.then.`Then the response should be` @@ -67,8 +68,8 @@ class `Citizen routes` : BaseTest() { `authenticated as`("Georges", "Charpak") `with body`(""" { - "old_password": "azerty", - "new_password": "qwerty" + "oldPassword": "azerty", + "newPassword": "qwerty" } """) } `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`() { withIntegrationApplication { `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") `with body`(""" { diff --git a/src/test/kotlin/integration/Comment articles routes.kt b/src/test/kotlin/integration/Comment articles routes.kt index d23082a..6bc4d51 100644 --- a/src/test/kotlin/integration/Comment articles routes.kt +++ b/src/test/kotlin/integration/Comment articles routes.kt @@ -84,9 +84,9 @@ class `Comment articles routes` : BaseTest() { `When I send a GET request`("/citizens/292a20cc-4a60-489e-9866-a95d38ffaf47/comments/articles") { } `Then the response should be` OK and { `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`("$.result[0]created_by.id", "292a20cc-4a60-489e-9866-a95d38ffaf47") + `And the response should contain`("$.result[0]createdBy.id", "292a20cc-4a60-489e-9866-a95d38ffaf47") } } } diff --git a/src/test/kotlin/integration/Comment constitutions routes.kt b/src/test/kotlin/integration/Comment constitutions routes.kt index 1ceb37c..eaa2020 100644 --- a/src/test/kotlin/integration/Comment constitutions routes.kt +++ b/src/test/kotlin/integration/Comment constitutions routes.kt @@ -15,7 +15,6 @@ import integration.steps.given.`Given I have constitution` import integration.steps.given.`authenticated as` import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.OK -import io.ktor.server.testing.setBody import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags 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") { } `Then the response should be` OK and { `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`("$.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 list`("$.result[*]", 1, 1) } diff --git a/src/test/kotlin/integration/Follow articles routes.kt b/src/test/kotlin/integration/Follow articles routes.kt index d0d0246..822c8e8 100644 --- a/src/test/kotlin/integration/Follow articles routes.kt +++ b/src/test/kotlin/integration/Follow articles routes.kt @@ -47,7 +47,7 @@ class `Follow articles routes` : BaseTest() { `authenticated as`("Johannes", "Kepler") } `Then the response should be` OK and { `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) } } diff --git a/src/test/kotlin/integration/Follow constitutions routes.kt b/src/test/kotlin/integration/Follow constitutions routes.kt index 53feae2..375f2f6 100644 --- a/src/test/kotlin/integration/Follow constitutions routes.kt +++ b/src/test/kotlin/integration/Follow constitutions routes.kt @@ -47,7 +47,7 @@ class `Follow constitutions routes` : BaseTest() { `authenticated as`("André-Marie", "Ampère") } `Then the response should be` OK and { `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) } } diff --git a/src/test/kotlin/integration/Register routes.kt b/src/test/kotlin/integration/Register routes.kt index 657386a..293f8fe 100644 --- a/src/test/kotlin/integration/Register routes.kt +++ b/src/test/kotlin/integration/Register routes.kt @@ -1,12 +1,15 @@ 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`.`with body` -import io.ktor.http.HttpStatusCode -import org.amshove.kluent.`should be null` -import org.amshove.kluent.`should contain` -import org.amshove.kluent.`should not be null` +import integration.steps.then.`And the response should be null` +import integration.steps.then.`And the response should contain pattern` +import integration.steps.then.`And the response 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.Tags import org.junit.jupiter.api.Test @@ -21,7 +24,7 @@ class `Register routes` : BaseTest() { `When I send a POST request`("/register") { `with body`(""" { - "name": {"first_name":"George", "last_name":"MICHEL"}, + "name": {"firstName":"George", "lastName":"MICHEL"}, "birthday": "2001-01-01", "user":{ "username": "george-junior", @@ -30,10 +33,9 @@ class `Register routes` : BaseTest() { "email": "george-junior@gmail.com" } """) - }.`Then the response should be`(HttpStatusCode.OK) { - content - .`should not be null`() - .`should contain`("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.") + } `Then the response should be` OK and { + `And the response should not be null`() + `And the response should contain pattern`("$.token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.") } } } @@ -41,19 +43,19 @@ class `Register routes` : BaseTest() { @Test fun `I cannot register if no username was sent`() { withIntegrationApplication { - `When I send a POST request`("/register") { + `When I send a POST request`("/register", Validate.RESPONSE_BODY) { `with body`(""" { - "name": {"first_name":"George2", "last_name":"MICHEL2"}, + "name": {"firstName":"George2", "lastName":"MICHEL2"}, "birthday": "2001-01-01", "user":{ - "username": "", "password": "" - } + }, + "email": "george-junior@gmail.com" } """) - }.`Then the response should be`(HttpStatusCode.BadRequest) { - content.`should be null`() + } `Then the response should be` BadRequest and { + `And the response should be null`() } } } diff --git a/src/test/kotlin/integration/Vote routes.kt b/src/test/kotlin/integration/Vote routes.kt index b3b2ae2..55c6263 100644 --- a/src/test/kotlin/integration/Vote routes.kt +++ b/src/test/kotlin/integration/Vote routes.kt @@ -65,7 +65,7 @@ class `Vote routes` : BaseTest() { `When I send a GET request`("/citizens/c044823d-e778-4256-9016-b1334bf933d3/votes/articles") { `authenticated as`("Carl", "Gauss") } `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`("$.total", 1) `And the response should contain`("$.result[0].note", 1) diff --git a/src/test/kotlin/integration/Workgroup routes.kt b/src/test/kotlin/integration/Workgroup routes.kt index fed5fc3..7db4e82 100644 --- a/src/test/kotlin/integration/Workgroup routes.kt +++ b/src/test/kotlin/integration/Workgroup routes.kt @@ -50,11 +50,11 @@ class `Workgroup routes` : BaseTest() { `And the response should contain`("$.id", "ab469134-bf14-4856-b093-ae1aa990f977") `And the response should contain`("$.name", "Les Mousquets") `And the response should contain`( - "$.members[*].citizen.name[?(@.first_name=='Stephen')].first_name", + "$.members[*].citizen.name[?(@.firstName=='Stephen')].firstName", "Stephen" ) `And the response should contain`( - "$.members[*].citizen.name[?(@.first_name=='Sadi')].first_name", + "$.members[*].citizen.name[?(@.firstName=='Sadi')].firstName", "Sadi" ) } diff --git a/src/test/kotlin/integration/steps/then/schema.kt b/src/test/kotlin/integration/steps/then/schema.kt index c12318f..81aa75a 100644 --- a/src/test/kotlin/integration/steps/then/schema.kt +++ b/src/test/kotlin/integration/steps/then/schema.kt @@ -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) { - operation(route) { api, uri -> +fun TestApplicationResponse.`And the schema response body must be valid`(contentType: ContentType? = ContentType.Application.Json) { + operation { api, uri -> /* Validate Response */ this.apply { val status = call.response.status() + val responseContent: JsonNode = if (content != null) + ObjectMapper().readTree(content) + else TextNode("") + getResponse(status?.value?.toString() ?: error("HttpStatus not found")) ?.getContentMediaType(contentType.toString()) ?.schema - ?.validate(api, ObjectMapper().readTree(content)) + ?.validate(api, responseContent) ?: 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 */ this.apply { Url(call.request.uri).parameters.forEach { parameter: String, values: List -> diff --git a/src/test/kotlin/integration/steps/when/request.kt b/src/test/kotlin/integration/steps/when/request.kt index a4ab480..94ba744 100644 --- a/src/test/kotlin/integration/steps/when/request.kt +++ b/src/test/kotlin/integration/steps/when/request.kt @@ -1,7 +1,9 @@ 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 response body must be valid` import io.ktor.application.ApplicationCall import io.ktor.http.ContentType 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.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) { method = HttpMethod.Get 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()) setup?.let { it() } - }.apply { - if (validate) { - response.`And the schema must be valid`() - requestBody?.let { body -> - response.`And the schema request body must be valid`(body) - } - } - } + }.valid(validate) } -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) { method = HttpMethod.Post 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.Accept, ContentType.Application.Json.toString()) setup?.let { it() } - }.apply { - if (validate) { - response.`And the schema must be valid`() - requestBody?.let { body -> - response.`And the schema request body must be valid`(body) - } - } - } + }.valid(validate) } -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) { method = HttpMethod.Put 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()) setup?.let { it() } - }.apply { - if (validate) { - response.`And the schema must be valid`() - requestBody?.let { body -> - response.`And the schema request body must be valid`(body) - } - } - } + }.valid(validate) } -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) { method = HttpMethod.Delete if (uri != null) { @@ -76,14 +81,7 @@ fun TestApplicationEngine.`When I send a DELETE request`(uri: String? = null, va setup?.let { it() }?.let { setBody(it.trimIndent()) } - }.apply { - if (validate) { - response.`And the schema must be valid`() - requestBody?.let { body -> - response.`And the schema request body must be valid`(body) - } - } - } + }.valid(validate) } private val requestBodies: MutableMap = mutableMapOf() diff --git a/src/test/resources/application-test.conf b/src/test/resources/application-test.conf index 7164edc..d860fb3 100644 --- a/src/test/resources/application-test.conf +++ b/src/test/resources/application-test.conf @@ -17,7 +17,7 @@ db { database = test username = test password = test - port = 5433 + port = 15432 } redis {