diff --git a/src/main/kotlin/fr/dcproject/repository/Vote.kt b/src/main/kotlin/fr/dcproject/repository/Vote.kt index f3f374c..0f24f34 100644 --- a/src/main/kotlin/fr/dcproject/repository/Vote.kt +++ b/src/main/kotlin/fr/dcproject/repository/Vote.kt @@ -3,10 +3,13 @@ package fr.dcproject.repository import fr.dcproject.entity.Article import fr.dcproject.entity.Constitution import fr.dcproject.entity.VoteAggregation +import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Requester import fr.postgresjson.entity.UuidEntity import fr.postgresjson.repository.RepositoryI +import java.util.* import kotlin.reflect.KClass +import fr.dcproject.entity.Citizen as CitizenEntity import fr.dcproject.entity.Vote as VoteEntity open class Vote (override var requester: Requester): RepositoryI> { @@ -26,6 +29,26 @@ open class Vote (override var requester: Requester): RepositoryI< "anonymous" to anonymous )!! } + + open fun findByCitizen( + citizen: CitizenEntity, + page: Int = 1, + limit: Int = 50 + ): Paginated> = + findByCitizen(citizen.id ?: error("The citizen must have an id"), page, limit) + + open fun findByCitizen( + citizenId: UUID, + page: Int = 1, + limit: Int = 50 + ): Paginated> { + return requester.run { + getFunction("find_votes_by_citizen") + .select(page, limit, + "created_by_id" to citizenId + ) + } + } } class VoteArticle (requester: Requester): Vote
(requester) diff --git a/src/main/kotlin/fr/dcproject/routes/PaginatedRequest.kt b/src/main/kotlin/fr/dcproject/routes/PaginatedRequest.kt new file mode 100644 index 0000000..c08d01b --- /dev/null +++ b/src/main/kotlin/fr/dcproject/routes/PaginatedRequest.kt @@ -0,0 +1,14 @@ +package fr.dcproject.routes + +interface PaginatedRequestI { + val page: Int + val limit: Int +} + +open class PaginatedRequest( + page: Int = 1, + limit: Int = 50 +): PaginatedRequestI { + override val page: Int = if (page < 1) 1 else page + override val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit +} \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/routes/VoteArticle.kt b/src/main/kotlin/fr/dcproject/routes/VoteArticle.kt index 3d43420..bffcb0f 100644 --- a/src/main/kotlin/fr/dcproject/routes/VoteArticle.kt +++ b/src/main/kotlin/fr/dcproject/routes/VoteArticle.kt @@ -4,11 +4,13 @@ import fr.dcproject.citizen import fr.dcproject.entity.Citizen import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest.Content import fr.dcproject.security.voter.VoteVoter.Action.CREATE +import fr.dcproject.security.voter.VoteVoter.Action.VIEW import fr.dcproject.security.voter.assertCan import io.ktor.application.call import io.ktor.http.HttpStatusCode import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location +import io.ktor.locations.get import io.ktor.locations.put import io.ktor.request.receive import io.ktor.response.respond @@ -19,10 +21,18 @@ import fr.dcproject.repository.VoteArticle as VoteArticleRepository @KtorExperimentalLocationsAPI object VoteArticlePaths { - @Location("/articles/{article}/vote") class ArticleVoteRequest(val article: ArticleEntity) { + @Location("/articles/{article}/vote") + class ArticleVoteRequest(val article: ArticleEntity) { data class Content(var note: Int) } - @Location("/citizens/{citizen}/votes/articles") class CitizenVoteArticleRequest(val citizen: Citizen) + + @Location("/citizens/{citizen}/votes/articles") + class CitizenVoteArticleRequest( + val citizen: Citizen, + page: Int = 1, + limit: Int = 50, + val search: String? = null + ): PaginatedRequestI by PaginatedRequest(page, limit) } @KtorExperimentalLocationsAPI @@ -38,4 +48,11 @@ fun Route.voteArticle(repo: VoteArticleRepository) { val votes = repo.vote(vote) call.respond(HttpStatusCode.Created, votes) } + + get { + val votes = repo.findByCitizen(it.citizen) + assertCan(VIEW, votes.result) + + call.respond(votes) + } } \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/security/voter/VoteVoter.kt b/src/main/kotlin/fr/dcproject/security/voter/VoteVoter.kt index 1c28ed6..e2cf46d 100644 --- a/src/main/kotlin/fr/dcproject/security/voter/VoteVoter.kt +++ b/src/main/kotlin/fr/dcproject/security/voter/VoteVoter.kt @@ -10,7 +10,10 @@ class VoteVoter: Voter { } override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean { - return action is Action && subject is VoteEntity<*>? + return action is Action && ( + subject is VoteEntity<*>? + || subject is List<*> + ) } override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote { @@ -19,8 +22,25 @@ class VoteVoter: Voter { return Vote.GRANTED } - if (action == Action.VIEW) { - return Vote.GRANTED + if (action == Action.VIEW && user != null) { + if (subject is VoteEntity<*>) { + return if (subject.createdBy?.userId != user.id) { + Vote.DENIED + } else { + Vote.GRANTED + } + } + + if (subject is List<*>) { + subject.forEach { + if (it !is VoteEntity<*> || it.createdBy?.userId != user.id) { + return Vote.DENIED + } + } + return Vote.GRANTED + } + + return Vote.DENIED } return Vote.ABSTAIN diff --git a/src/test/kotlin/VoteTest.kt b/src/test/kotlin/VoteTest.kt new file mode 100644 index 0000000..97b3362 --- /dev/null +++ b/src/test/kotlin/VoteTest.kt @@ -0,0 +1,130 @@ +import fr.dcproject.entity.Article +import fr.dcproject.entity.Citizen +import fr.dcproject.entity.User +import fr.dcproject.entity.Vote +import fr.postgresjson.serializer.deserialize +import fr.postgresjson.serializer.serialize +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.util.KtorExperimentalAPI +import org.amshove.kluent.`should equal` +import org.amshove.kluent.shouldBe +import org.intellij.lang.annotations.Language +import org.joda.time.DateTime +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS + +@KtorExperimentalLocationsAPI +@KtorExperimentalAPI +@TestInstance(PER_CLASS) +class VoteTest { + @Language("JSON") + private val voteJson: String = """ + { + "id": "032acc3d-e8c5-4cb2-9297-bec913ff8d9b", + "created_by": { + "id": "40c65a43-f9f8-45cd-aa31-953376e7c94a", + "name": { + "first_name": "Jaque", + "last_name": "Bono", + "civility": null + }, + "birthday": "2019-10-01T10:59:40.570Z", + "user_id": null, + "vote_anonymous": true, + "follow_anonymous": true, + "user": { + "id": "f68df389-fb0d-423e-90fd-a140a9ed29b9", + "username": "jaque", + "blocked_at": null, + "plain_password": "azerty", + "roles": [], + "created_at": null, + "updated_at": null + }, + "deleted": false, + "created_at": null, + "deleted_at": null + }, + "target": { + "id": "90f28912-7bd5-4f37-a0ea-8620e3817d51", + "title": "Hello world!", + "anonymous": true, + "content": "bla bla bla", + "description": "this is the changement !", + "tags": [], + "draft": false, + "last_version": false, + "created_by": { + "id": "40c65a43-f9f8-45cd-aa31-953376e7c94a", + "name": { + "first_name": "Jaque", + "last_name": "Bono", + "civility": null + }, + "birthday": "2019-10-01T10:59:40.570Z", + "user_id": null, + "vote_anonymous": true, + "follow_anonymous": true, + "user": { + "id": "f68df389-fb0d-423e-90fd-a140a9ed29b9", + "username": "jaque", + "blocked_at": null, + "plain_password": "azerty", + "roles": [], + "created_at": null, + "updated_at": null + }, + "deleted": false, + "created_at": null, + "deleted_at": null + }, + "votes": { + "up": 0, + "neutral": 0, + "down": 0, + "updated_at": null + }, + "version_id": "48dad61e-c54b-4f4c-9f66-428f90b94045", + "version_number": null, + "deleted": false, + "created_at": null, + "deleted_at": null + }, + "note": -1, + "anonymous": true, + "updated_at": null, + "created_at": null + }""".trimIndent() + + @Test + fun `test Vote Article serialize`() { + val user = User(username = "jaque", plainPassword = "azerty") + val citizen = Citizen( + name = Citizen.Name("Jaque", "Bono"), + birthday = DateTime.now(), + user = user + ) + val article = Article( + title = "Hello world!", + content = "bla bla bla", + description = "this is the changement !", + createdBy = citizen + ) + val vote = Vote( + createdBy = citizen, + target = article, + note = -1 + ) + vote.serialize().contains("""Hello world!""") shouldBe true + vote.serialize().contains("-1") shouldBe true + println(vote.serialize()) + } + + @Test + fun `test Vote Article Deserialize`() { + val vote: Vote
= voteJson.deserialize()!! + vote.id.toString() `should equal` "032acc3d-e8c5-4cb2-9297-bec913ff8d9b" + vote.note.toString() `should equal` "-1" + } +} diff --git a/src/test/resources/feature/vote.feature b/src/test/resources/feature/vote.feature index 7f67c8f..446de80 100644 --- a/src/test/resources/feature/vote.feature +++ b/src/test/resources/feature/vote.feature @@ -1,6 +1,6 @@ Feature: vote Article - Scenario: Vote article + Scenario: Can Vote article Given I am authenticated as John Doe with id "64b7b379-2298-43ec-b428-ba134930cabd" When I send a PUT request to "/articles/9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b/vote" with body: """ @@ -10,7 +10,7 @@ Feature: vote Article """ Then the response status code should be 201 - Scenario: Vote constitution + Scenario: Can Vote constitution Given I am authenticated as John Doe with id "64b7b379-2298-43ec-b428-ba134930cabd" When I send a PUT request to "/constitutions/64b1f265-bfb3-332b-eef9-d00f63a3beaa/vote" with body: """ @@ -19,3 +19,13 @@ Feature: vote Article } """ Then the response status code should be 201 + + Scenario: Can get votes of current citizen + Given I am authenticated as John Doe with id "64b7b379-2298-43ec-b428-ba134930cabd" + When I send a GET request to "/citizens/64b7b379-2298-43ec-b428-ba134930cabd/votes/articles" + Then the response status code should be 200 + And the response should contain object: + | current_page | 1 | + | limit | 50 | + | total | 2 | + | result[0].note | -1 |