Valider les resource entrente #91

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

View File

@@ -63,12 +63,14 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
constructor(
createdBy: C,
parent: CommentParent<T>,
content: String
content: String,
id: UUID? = null,
) : this(
createdBy = createdBy,
parent = parent,
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,
limit: Int = 50,
sort: String = "createdAt"
): Paginated<CommentForView<T, CitizenCreatorI>> {
return requester.run {
getFunction("find_comments_by_target")
.select<CommentForView<T, CitizenCreator>>(
page,
limit,
"target_id" to targetId,
"sort" to sort
)
as Paginated<CommentForView<T, CitizenCreatorI>>
}
}
): Paginated<CommentForView<T, CitizenCreatorI>> = requester
.getFunction("find_comments_by_target")
.select<CommentForView<T, CitizenCreator>>(
page,
limit,
"target_id" to targetId,
"sort" to sort
) as Paginated<CommentForView<T, CitizenCreatorI>>
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>) {
requester
.getFunction("comment")
.sendQuery(
"reference" to comment.target.reference,
"resource" to comment
)
}
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>): CommentForView<TargetRef, CitizenCreator> = requester
.getFunction("comment")
.selectOne(
"reference" to comment.target.reference,
"resource" to comment
)!!
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>) {
requester
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>): CommentForView<TargetRef, CitizenCreator> {
return requester
.getFunction("edit_comment")
.sendQuery(
.selectOne(
"id" to comment.id,
"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
import fr.dcproject.common.response.toOutput
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
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
@@ -24,22 +28,40 @@ object EditComment {
@Location("/comments/{comment}")
class EditCommentRequest(comment: UUID) {
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) {
put<EditCommentRequest> {
mustBeAuth()
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.assert { canUpdate(comment, citizenOrNull) }
val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.assert { canUpdate(commentOld, citizenOrNull) }
comment.content = call.receiveOrBadRequest<EditCommentRequest.Input>().content
repo.edit(comment)
call.respond(
HttpStatusCode.OK,
comment.toOutput()
)
call.receiveOrBadRequest<EditCommentRequest.Input>()
.apply { validate().badRequestIfNotValid() }
.run {
CommentForUpdate(
id = commentOld.id,
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
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.GetCommentChildren.getChildrenComments
import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment

View File

@@ -598,6 +598,40 @@ paths:
application/json:
schema:
$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:
security:
- JWTAuth: []
@@ -614,7 +648,7 @@ paths:
content:
type: string
example:
Lorem ipsum...
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
responses:
200:
description: Return updated comment
@@ -622,6 +656,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'
/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
$$
declare
@@ -17,7 +17,8 @@ begin
else
raise exception 'comment with target as "%", is not implemented', reference::text;
end if;
_id = _new_id;
select find_comment_by_id(_new_id) into resource;
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
$$
begin
update comment c set
"content" = _content
where c.id = _id;
select find_comment_by_id(_id) into resource;
end;
$$;

View File

@@ -2,16 +2,20 @@ package integration
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 POST request`
import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have article`
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 comment`
import integration.steps.given.`authenticated as`
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
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
fun `I can edit comment`() {
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")
`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": "Hello boy"
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}
"""
)
} `Then the response should be` OK and {
`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 fr.dcproject.common.entity.TargetI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.utils.toUUID
import fr.dcproject.component.article.database.ArticleRef
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.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.constitution.database.ConstitutionRef
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,
article: ArticleRef? = null,
article: A? = null,
createdBy: Name? = null,
content: String? = null
) {
): CommentForView<TargetRef, CitizenCreator> {
val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() }
createCommentOnTarget(
return createCommentOnTarget(
id,
article?.id?.let { articleRepository.findById(article.id) } ?: createArticle(article?.id),
createdBy,
@@ -56,14 +61,14 @@ fun TestApplicationEngine.`Given I have comment on constitution`(
createComment(id?.toUUID(), ConstitutionRef(constitution?.toUUID()), createdBy, content)
}
fun createComment(
fun <C : ConstitutionRef> createComment(
id: UUID? = null,
constitution: ConstitutionRef? = null,
constitution: C? = null,
createdBy: Name? = null,
content: String? = null
) {
): CommentForView<TargetRef, CitizenCreator> {
val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().koin.get() }
createCommentOnTarget(
return createCommentOnTarget(
id,
constitution?.id?.let { constitutionRepository.findById(constitution.id) } ?: createConstitution(constitution?.id),
createdBy,
@@ -71,12 +76,12 @@ fun createComment(
)
}
fun createCommentOnTarget(
fun <T : TargetI> createCommentOnTarget(
id: UUID? = null,
target: TargetI,
target: T,
createdBy: Name? = null,
content: String? = null
) {
): CommentForView<TargetRef, CitizenCreator> {
val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() }
val creator = createCitizen(createdBy)
val comment = CommentForUpdate(
@@ -85,5 +90,41 @@ fun createCommentOnTarget(
target = target,
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)
}