Valider les resource entrente #91

Merged
flecomte merged 46 commits from 21-valid-input into master 2021-04-16 03:27:11 +02:00
4 changed files with 162 additions and 21 deletions
Showing only changes of commit 34513e25b6 - Show all commits

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.comment.constitution.routes package fr.dcproject.component.comment.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
@@ -12,6 +13,9 @@ import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -26,27 +30,37 @@ object CreateConstitutionComment {
@Location("/constitutions/{constitution}/comments") @Location("/constitutions/{constitution}/comments")
class CreateConstitutionCommentRequest(constitution: UUID) { class CreateConstitutionCommentRequest(constitution: UUID) {
val constitution = ConstitutionRef(constitution) val constitution = ConstitutionRef(constitution)
class Input(val content: String) class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
} }
fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) { fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
post<CreateConstitutionCommentRequest> { post<CreateConstitutionCommentRequest> {
mustBeAuth() mustBeAuth()
call.receiveOrBadRequest<Input>().run {
CommentForUpdate(
target = it.constitution,
createdBy = citizen,
content = content
)
}.let { comment ->
ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
call.respond( call.receiveOrBadRequest<Input>()
HttpStatusCode.Created, .apply { validate().badRequestIfNotValid() }
comment.toOutput() .run {
) CommentForUpdate(
} target = it.constitution,
createdBy = citizen,
content = content
)
}.let { comment ->
ac.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
call.respond(
HttpStatusCode.Created,
comment.toOutput()
)
}
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.comment.constitution.routes package fr.dcproject.component.comment.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
@@ -7,6 +8,12 @@ import fr.dcproject.component.comment.constitution.database.CommentConstitutionR
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -19,12 +26,36 @@ import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object GetConstitutionComment { object GetConstitutionComment {
@Location("/constitutions/{constitution}/comments") @Location("/constitutions/{constitution}/comments")
class GetConstitutionCommentRequest(constitution: UUID) { class GetConstitutionCommentRequest(
constitution: UUID,
page: Int = 1,
limit: Int = 50,
val search: String? = null,
val sort: String = "createdAt"
) : PaginatedRequestI by PaginatedRequest(page, limit) {
val constitution = ConstitutionRef(constitution) val constitution = ConstitutionRef(constitution)
fun validate() = Validation<GetConstitutionCommentRequest> {
GetConstitutionCommentRequest::page {
minimum(1)
}
GetConstitutionCommentRequest::limit {
minimum(1)
maximum(50)
}
GetConstitutionCommentRequest::sort ifPresent {
enum(
"votes",
"createdAt",
)
}
}.validate(this)
} }
fun Route.getConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) { fun Route.getConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
get<GetConstitutionCommentRequest> { get<GetConstitutionCommentRequest> {
it.validate().badRequestIfNotValid()
val comments = repo.findByTarget(it.constitution) val comments = repo.findByTarget(it.constitution)
ac.assert { canView(comments.result, citizenOrNull) } ac.assert { canView(comments.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -522,13 +522,13 @@ paths:
in: query in: query
required: false required: false
example: example:
- created_at - createdAt
- votes - votes
schema: schema:
type: string type: string
default: created_at default: createdAt
enum: enum:
- created_at - createdAt
- votes - votes
responses: responses:
200: 200:
@@ -707,13 +707,42 @@ paths:
tags: tags:
- comment - comment
- constitution - constitution
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/search'
- name: sort
in: query
required: false
example:
- createdAt
- votes
schema:
type: string
default: createdAt
enum:
- createdAt
- votes
responses: responses:
200: 200:
description: Return Comment and children description: Return Comment and children
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' allOf:
- $ref: '#/components/schemas/Paginated'
- type: object
properties:
result:
type: array
items:
$ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [] - JWTAuth: []
@@ -739,6 +768,12 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'

View File

@@ -1,6 +1,9 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.`when`.Validate
import integration.steps.`when`.Validate.ALL
import integration.steps.`when`.Validate.REQUEST_BODY
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
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`
@@ -13,6 +16,7 @@ import integration.steps.then.`And the response should contain`
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`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest
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 org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -33,12 +37,69 @@ class `Comment constitutions routes` : BaseTest() {
`with body`( `with body`(
""" """
{ {
"content": "Hello mister" "content": "Hello mister MARABOUTCHA"
} }
""" """
) )
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.target.id", "1707c287-a472-4a62-89f2-9e85030e915c")
`And the response should contain`("$.content", "Hello mister MARABOUTCHA")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot comment constitution with bad request`() {
withIntegrationApplication {
`Given I have citizen`("Nicolas", "Copernic")
`Given I have constitution`(id = "aa16c635-28da-46f0-9a89-934eef88c7ca")
`When I send a POST request`("/constitutions/aa16c635-28da-46f0-9a89-934eef88c7ca/comments", ALL - REQUEST_BODY) {
`authenticated as`("Nicolas", "Copernic")
`with body`(
"""
{
"content": "To small content"
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".content")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
}
}
}
@Test
fun `I can get all comment on constitution`() {
withIntegrationApplication {
`Given I have citizen`("Enrico", "Fermi")
`Given I have constitution`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
`Given I have comment on constitution`(constitution = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
`When I send a GET request`("/constitutions/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=votes") {
`authenticated as`("Enrico", "Fermi")
} `Then the response should be` OK and {
`And the response should not be null`()
`And the response should contain`("$.result[0].target.id", "6166c078-ca97-4366-b0aa-2a5cd558c78a")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot get all comment on constitution with wrong parameters`() {
withIntegrationApplication {
`Given I have citizen`("Enrico", "Fermi")
`Given I have constitution`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
`Given I have comment on constitution`(constitution = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
`When I send a GET request`("/constitutions/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=wrong", ALL - Validate.REQUEST_PARAM) {
`authenticated as`("Enrico", "Fermi")
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[*].name", ".sort")
`And the response should contain`("$.invalidParams[*].reason", "must be one of: 'votes', 'createdAt'")
} }
} }
} }