Add validation on route CreateComments & EditComment

rename POST /comments/{comment}/children
method edit and create comment of repository return edited/created comment
This commit is contained in:
2021-04-10 01:16:09 +02:00
parent 27e405c585
commit 8223dd21bb
11 changed files with 307 additions and 107 deletions

View File

@@ -63,12 +63,14 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
constructor( constructor(
createdBy: C, createdBy: C,
parent: CommentParent<T>, parent: CommentParent<T>,
content: String content: String,
id: UUID? = null,
) : this( ) : this(
createdBy = createdBy, createdBy = createdBy,
parent = parent, parent = parent,
target = parent.target, target = parent.target,
content = content content = content,
id = id ?: UUID.randomUUID(),
) )
} }

View File

@@ -58,35 +58,29 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
sort: String = "createdAt" sort: String = "createdAt"
): Paginated<CommentForView<T, CitizenCreatorI>> { ): Paginated<CommentForView<T, CitizenCreatorI>> = requester
return requester.run { .getFunction("find_comments_by_target")
getFunction("find_comments_by_target") .select<CommentForView<T, CitizenCreator>>(
.select<CommentForView<T, CitizenCreator>>( page,
page, limit,
limit, "target_id" to targetId,
"target_id" to targetId, "sort" to sort
"sort" to sort ) as Paginated<CommentForView<T, CitizenCreatorI>>
)
as Paginated<CommentForView<T, CitizenCreatorI>>
}
}
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>) { fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>): CommentForView<TargetRef, CitizenCreator> = requester
requester .getFunction("comment")
.getFunction("comment") .selectOne(
.sendQuery( "reference" to comment.target.reference,
"reference" to comment.target.reference, "resource" to comment
"resource" to comment )!!
)
}
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>) { fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>): CommentForView<TargetRef, CitizenCreator> {
requester return requester
.getFunction("edit_comment") .getFunction("edit_comment")
.sendQuery( .selectOne(
"id" to comment.id, "id" to comment.id,
"content" to comment.content "content" to comment.content
) )!!
} }
} }

View File

@@ -0,0 +1,63 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
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.features.NotFoundException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object CreateComment {
@Location("/comments/{comment}")
class CreateCommentRequest(comment: UUID) {
val comment = CommentRef(comment)
class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
}
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
post<CreateCommentRequest> {
mustBeAuth()
call.receiveOrBadRequest<CreateCommentRequest.Input>()
.apply { validate().badRequestIfNotValid() }
.run {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
CommentForUpdate(
content = content,
createdBy = citizen,
target = parent.target,
parent = parent,
)
}.let { newComment ->
ac.assert { canCreate(newComment, citizenOrNull) }
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment.toOutput())
}
}
}
}

View File

@@ -1,47 +0,0 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object CreateCommentChildren {
@Location("/comments/{comment}/children")
class CreateCommentChildrenRequest(comment: UUID) {
val comment = CommentRef(comment)
class Input(val content: String)
}
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
post<CreateCommentChildrenRequest> {
mustBeAuth()
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = CommentForUpdate(
content = call.receiveOrBadRequest<CreateCommentChildrenRequest.Input>().content,
createdBy = citizen,
parent = parent
)
ac.assert { canCreate(newComment, citizenOrNull) }
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment.toOutput())
}
}
}

View File

@@ -1,14 +1,18 @@
package fr.dcproject.component.comment.generic.routes package fr.dcproject.component.comment.generic.routes
import fr.dcproject.common.response.toOutput import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
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.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -24,22 +28,40 @@ object EditComment {
@Location("/comments/{comment}") @Location("/comments/{comment}")
class EditCommentRequest(comment: UUID) { class EditCommentRequest(comment: UUID) {
val comment = CommentRef(comment) val comment = CommentRef(comment)
class Input(val content: String) class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
} }
fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) { fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) {
put<EditCommentRequest> { put<EditCommentRequest> {
mustBeAuth() mustBeAuth()
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.assert { canUpdate(comment, citizenOrNull) } ac.assert { canUpdate(commentOld, citizenOrNull) }
comment.content = call.receiveOrBadRequest<EditCommentRequest.Input>().content call.receiveOrBadRequest<EditCommentRequest.Input>()
repo.edit(comment) .apply { validate().badRequestIfNotValid() }
.run {
call.respond( CommentForUpdate(
HttpStatusCode.OK, id = commentOld.id,
comment.toOutput() createdBy = commentOld.createdBy,
) target = commentOld.target,
parent = commentOld.parent,
content = content,
)
}
.let { repo.edit(it) }
.let {
call.respond(
HttpStatusCode.OK,
it.toOutput()
)
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.comment.generic.routes package fr.dcproject.component.comment.generic.routes
import fr.dcproject.component.comment.generic.routes.CreateCommentChildren.createCommentChildren import fr.dcproject.component.comment.generic.routes.CreateComment.createCommentChildren
import fr.dcproject.component.comment.generic.routes.EditComment.editComment import fr.dcproject.component.comment.generic.routes.EditComment.editComment
import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments
import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment

View File

@@ -598,6 +598,40 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
post:
security:
- JWTAuth: []
summary: create comment
tags:
- comment
requestBody:
content:
application/json:
schema:
required:
- content
properties:
content:
type: string
example:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
responses:
201:
description: Return updated comment
content:
application/json:
schema:
$ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401:
$ref: '#/components/responses/401'
404:
description: No comment found
put: put:
security: security:
- JWTAuth: [] - JWTAuth: []
@@ -614,7 +648,7 @@ paths:
content: content:
type: string type: string
example: example:
Lorem ipsum... Lorem ipsum dolor sit amet, consectetur adipiscing elit.
responses: responses:
200: 200:
description: Return updated comment description: Return updated comment
@@ -622,6 +656,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'
/comments/{comment}/children: /comments/{comment}/children:

View File

@@ -1,4 +1,4 @@
create or replace function comment(reference regclass, resource json, out _id uuid) create or replace function comment(reference regclass, inout resource json)
language plpgsql as language plpgsql as
$$ $$
declare declare
@@ -17,7 +17,8 @@ begin
else else
raise exception 'comment with target as "%", is not implemented', reference::text; raise exception 'comment with target as "%", is not implemented', reference::text;
end if; end if;
_id = _new_id;
select find_comment_by_id(_new_id) into resource;
end; end;
$$; $$;

View File

@@ -1,9 +1,11 @@
create or replace function edit_comment(_id uuid, _content text) returns void create or replace function edit_comment(_id uuid, _content text, out resource json)
language plpgsql as language plpgsql as
$$ $$
begin begin
update comment c set update comment c set
"content" = _content "content" = _content
where c.id = _id; where c.id = _id;
select find_comment_by_id(_id) into resource;
end; end;
$$; $$;

View File

@@ -2,16 +2,20 @@ package integration
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
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 PUT request` import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have comment on article` import integration.steps.given.`Given I have comment on article`
import integration.steps.given.`Given I have comment on comment`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain` 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.OK 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
@@ -35,27 +39,105 @@ class `Comment routes` : BaseTest() {
} }
} }
@Test
fun `I can create comment`() {
withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves")
`Given I have comment on comment`(id = "49933147-fc0f-4e5c-aa8d-f77fa0d88fa6")
`When I send a POST request`("/comments/49933147-fc0f-4e5c-aa8d-f77fa0d88fa6") {
`authenticated as`("Hubert", "Reeves")
`with body`(
"""
{
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}
"""
)
} `Then the response should be` Created and {
`And the response should not be null`()
`And the response should contain`("$.content", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot create comment with bad request`() {
withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves")
`Given I have comment on comment`(id = "49933147-fc0f-4e5c-aa8d-f77fa0d88fa6")
`When I send a POST request`("/comments/49933147-fc0f-4e5c-aa8d-f77fa0d88fa6") {
`authenticated as`("Hubert", "Reeves")
`with body`(
"""
{
"content": "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 @Test
fun `I can edit comment`() { fun `I can edit comment`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves") `Given I have citizen`("Hubert", "Reeves")
`Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1") `Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1")
`Given I have comment on article`(article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1", createdBy = CitizenI.Name( `Given I have comment on article`(
"Hubert", article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1",
"Reeves" createdBy = CitizenI.Name(
), id = "fd30d20f-656c-42c6-8955-f61c04537464") "Hubert",
"Reeves"
),
id = "fd30d20f-656c-42c6-8955-f61c04537464"
)
`When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464") { `When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464") {
`authenticated as`("Hubert", "Reeves") `authenticated as`("Hubert", "Reeves")
`with body`( `with body`(
""" """
{ {
"content": "Hello boy" "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
} }
""" """
) )
} `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`("$.content", "Hello boy") `And the response should contain`("$.content", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
}
}
}
@Test
fun `I cannot edit comment with bad request`() {
withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves")
`Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1")
`Given I have comment on article`(
article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1",
createdBy = CitizenI.Name(
"Hubert",
"Reeves"
),
id = "fd30d20f-656c-42c6-8955-f61c04537464"
)
`When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464") {
`authenticated as`("Hubert", "Reeves")
`with body`(
"""
{
"content": "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")
} }
} }
} }

View File

@@ -2,11 +2,16 @@ package integration.steps.given
import com.thedeanda.lorem.LoremIpsum import com.thedeanda.lorem.LoremIpsum
import fr.dcproject.common.entity.TargetI import fr.dcproject.common.entity.TargetI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.utils.toUUID import fr.dcproject.common.utils.toUUID
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import fr.dcproject.component.comment.generic.database.CommentForUpdate import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentForView
import fr.dcproject.component.comment.generic.database.CommentI
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.constitution.database.ConstitutionRepository import fr.dcproject.component.constitution.database.ConstitutionRepository
@@ -32,14 +37,14 @@ fun TestApplicationEngine.`Given I have comments on article`(
} }
} }
fun createComment( fun <A : ArticleRef> createComment(
id: UUID? = null, id: UUID? = null,
article: ArticleRef? = null, article: A? = null,
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
) { ): CommentForView<TargetRef, CitizenCreator> {
val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() } val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() }
createCommentOnTarget( return createCommentOnTarget(
id, id,
article?.id?.let { articleRepository.findById(article.id) } ?: createArticle(article?.id), article?.id?.let { articleRepository.findById(article.id) } ?: createArticle(article?.id),
createdBy, createdBy,
@@ -56,14 +61,14 @@ fun TestApplicationEngine.`Given I have comment on constitution`(
createComment(id?.toUUID(), ConstitutionRef(constitution?.toUUID()), createdBy, content) createComment(id?.toUUID(), ConstitutionRef(constitution?.toUUID()), createdBy, content)
} }
fun createComment( fun <C : ConstitutionRef> createComment(
id: UUID? = null, id: UUID? = null,
constitution: ConstitutionRef? = null, constitution: C? = null,
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
) { ): CommentForView<TargetRef, CitizenCreator> {
val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().koin.get() } val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().koin.get() }
createCommentOnTarget( return createCommentOnTarget(
id, id,
constitution?.id?.let { constitutionRepository.findById(constitution.id) } ?: createConstitution(constitution?.id), constitution?.id?.let { constitutionRepository.findById(constitution.id) } ?: createConstitution(constitution?.id),
createdBy, createdBy,
@@ -71,12 +76,12 @@ fun createComment(
) )
} }
fun createCommentOnTarget( fun <T : TargetI> createCommentOnTarget(
id: UUID? = null, id: UUID? = null,
target: TargetI, target: T,
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
) { ): CommentForView<TargetRef, CitizenCreator> {
val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() } val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() }
val creator = createCitizen(createdBy) val creator = createCitizen(createdBy)
val comment = CommentForUpdate( val comment = CommentForUpdate(
@@ -85,5 +90,41 @@ fun createCommentOnTarget(
target = target, target = target,
content = content ?: LoremIpsum().getParagraphs(1, 3) content = content ?: LoremIpsum().getParagraphs(1, 3)
) )
commentRepository.comment(comment) return commentRepository.comment(comment)
}
fun TestApplicationEngine.`Given I have comment on comment`(
id: String? = null,
parent: String? = null,
createdBy: Name? = null,
content: String? = null,
): CommentForView<out TargetRef, CitizenCreator> {
return createCommentOnComment(
id?.toUUID() ?: UUID.randomUUID(),
parent?.run { CommentRef(toUUID()) },
createdBy,
content,
)
}
fun createCommentOnComment(
id: UUID? = null,
parent: CommentI? = createComment<ArticleRef>(),
createdBy: Name? = null,
content: String? = null
): CommentForView<out TargetRef, CitizenCreator> {
val creator = createCitizen(createdBy)
val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() }
val parentComment = if (parent == null) {
createComment<ArticleRef>()
} else {
commentRepository.findById(parent.id) ?: error("Parent of comment not found")
}
val comment = CommentForUpdate(
id = id ?: UUID.randomUUID(),
createdBy = creator,
content = content ?: LoremIpsum().getParagraphs(1, 3),
parent = parentComment,
)
return commentRepository.comment(comment)
} }