diff --git a/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/CreateConstitutionComment.kt b/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/CreateConstitutionComment.kt index cf368e4..0b700f0 100644 --- a/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/CreateConstitutionComment.kt +++ b/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/CreateConstitutionComment.kt @@ -1,5 +1,6 @@ package fr.dcproject.component.comment.constitution.routes +import fr.dcproject.application.http.badRequestIfNotValid import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert 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.toOutput 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.http.HttpStatusCode import io.ktor.locations.KtorExperimentalLocationsAPI @@ -26,27 +30,37 @@ object CreateConstitutionComment { @Location("/constitutions/{constitution}/comments") class CreateConstitutionCommentRequest(constitution: UUID) { val constitution = ConstitutionRef(constitution) - class Input(val content: String) + class Input(val content: String) { + fun validate() = Validation { + Input::content { + minLength(20) + maxLength(6000) + } + }.validate(this) + } } fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) { post { mustBeAuth() - call.receiveOrBadRequest().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() - ) - } + call.receiveOrBadRequest() + .apply { validate().badRequestIfNotValid() } + .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() + ) + } } } } diff --git a/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/GetConstitutionComment.kt b/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/GetConstitutionComment.kt index 40ae7ef..47d417e 100644 --- a/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/GetConstitutionComment.kt +++ b/src/main/kotlin/fr/dcproject/component/comment/constitution/routes/GetConstitutionComment.kt @@ -1,5 +1,6 @@ package fr.dcproject.component.comment.constitution.routes +import fr.dcproject.application.http.badRequestIfNotValid import fr.dcproject.common.response.toOutput import fr.dcproject.common.security.assert 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.toOutput 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.http.HttpStatusCode import io.ktor.locations.KtorExperimentalLocationsAPI @@ -19,12 +26,36 @@ import java.util.UUID @KtorExperimentalLocationsAPI object GetConstitutionComment { @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) + + fun validate() = Validation { + 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) { get { + it.validate().badRequestIfNotValid() + val comments = repo.findByTarget(it.constitution) ac.assert { canView(comments.result, citizenOrNull) } call.respond( diff --git a/src/main/resources/openapi.yaml b/src/main/resources/openapi.yaml index ab15282..c0a4348 100644 --- a/src/main/resources/openapi.yaml +++ b/src/main/resources/openapi.yaml @@ -522,13 +522,13 @@ paths: in: query required: false example: - - created_at + - createdAt - votes schema: type: string - default: created_at + default: createdAt enum: - - created_at + - createdAt - votes responses: 200: @@ -707,13 +707,42 @@ paths: tags: - comment - 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: 200: description: Return Comment and children content: application/json: 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: security: - JWTAuth: [] @@ -739,6 +768,12 @@ paths: application/json: schema: $ref: '#/components/schemas/CommentResponse' + 400: + description: BadReqest + content: + application/json: + schema: + $ref: '#/components/schemas/400' 401: $ref: '#/components/responses/401' diff --git a/src/test/kotlin/integration/Comment constitutions routes.kt b/src/test/kotlin/integration/Comment constitutions routes.kt index a999082..523a0be 100644 --- a/src/test/kotlin/integration/Comment constitutions routes.kt +++ b/src/test/kotlin/integration/Comment constitutions routes.kt @@ -1,6 +1,9 @@ package integration 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 POST request` 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.`Then the response should be` 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.OK import org.junit.jupiter.api.Tag @@ -33,12 +37,69 @@ class `Comment constitutions routes` : BaseTest() { `with body`( """ { - "content": "Hello mister" + "content": "Hello mister MARABOUTCHA" } """ ) } `Then the response should be` Created and { `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'") } } }