Refactoring of VoteVoter

This commit is contained in:
2021-01-17 23:32:43 +01:00
parent 308a284280
commit d6840e8064
6 changed files with 57 additions and 144 deletions

View File

@@ -96,7 +96,6 @@ fun Application.module(env: Env = PROD) {
install(AuthorizationVoter) {
voters = listOf(
VoteVoter(),
FollowVoter(),
OpinionVoter(),
OpinionChoiceVoter()
@@ -211,8 +210,8 @@ fun Application.module(env: Env = PROD) {
followArticle(get())
followConstitution(get())
commentConstitution(get(), get())
voteArticle(get(), get(), get())
voteConstitution(get())
voteArticle(get(), get(), get(), get())
voteConstitution(get(), get())
opinionArticle(get())
opinionChoice(get())
definition()

View File

@@ -24,6 +24,7 @@ import fr.dcproject.messages.Mailer
import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.repository.CommentConstitutionRepository
import fr.dcproject.security.voter.ConstitutionVoter
import fr.dcproject.security.voter.VoteVoter
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
@@ -125,6 +126,7 @@ val KoinModule = module {
single { CommentVoter() }
single { WorkgroupVoter() }
single { ConstitutionVoter() }
single { VoteVoter() }
// Elasticsearch Client
single<RestClient> {

View File

@@ -2,17 +2,16 @@ package fr.dcproject.routes
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.repository.VoteComment
import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest
import fr.dcproject.routes.VoteArticlePaths.CommentVoteRequest
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
import fr.dcproject.security.voter.VoteVoter.Action.VIEW
import fr.dcproject.security.voter.VoteVoter
import fr.dcproject.utils.toUUID
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
import fr.dcproject.voter.assert
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.locations.*
@@ -49,7 +48,7 @@ object VoteArticlePaths {
}
@KtorExperimentalLocationsAPI
fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentRepository) {
fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentRepository, voter: VoteVoter) {
put<ArticleVoteRequest> {
val content = call.receive<ArticleVoteRequest.Content>()
val vote = VoteForUpdate(
@@ -57,7 +56,7 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
note = content.note,
createdBy = this.citizen
)
assertCan(CREATE, vote)
voter.assert { canCreate(vote, citizenOrNull) }
val votes = repo.vote(vote)
call.respond(HttpStatusCode.Created, votes)
}
@@ -70,14 +69,14 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
note = content.note,
createdBy = this.citizen
)
assertCan(CREATE, vote)
voter.assert { canCreate(vote, citizenOrNull) }
val votes = voteCommentRepo.vote(vote)
call.respond(HttpStatusCode.Created, votes)
}
get<VoteArticlePaths.CitizenVoteArticleRequest> {
val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
assertCanAll(VIEW, votes.result)
voter.assert { canView(votes.result, citizenOrNull) }
call.respond(votes)
}
@@ -85,7 +84,7 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
get<VoteArticlePaths.CitizenVotesByIdsRequest> {
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
if (votes.isNotEmpty()) {
assertCanAll(VIEW, votes)
voter.assert { canView(votes, citizenOrNull) }
}
call.respond(votes)
}

View File

@@ -1,11 +1,12 @@
package fr.dcproject.routes
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
import fr.ktorVoter.assertCan
import fr.dcproject.security.voter.VoteVoter
import fr.dcproject.voter.assert
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.locations.*
@@ -27,7 +28,7 @@ object VoteConstitutionPaths {
}
@KtorExperimentalLocationsAPI
fun Route.voteConstitution(repo: VoteConstitutionRepository) {
fun Route.voteConstitution(repo: VoteConstitutionRepository, voter: VoteVoter) {
put<VoteConstitutionPaths.ConstitutionVoteRequest> {
val content = call.receive<Content>()
val vote = VoteForUpdate(
@@ -35,7 +36,7 @@ fun Route.voteConstitution(repo: VoteConstitutionRepository) {
note = content.note,
createdBy = this.citizen
)
assertCan(CREATE, vote)
voter.assert { canCreate(vote, citizenOrNull) }
repo.vote(vote)
call.respond(HttpStatusCode.Created)
}

View File

@@ -1,50 +1,26 @@
package fr.dcproject.security.voter
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.VoteForUpdateI
import fr.dcproject.entity.VoteI
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
import fr.postgresjson.entity.EntityDeletedAt
import io.ktor.application.*
import fr.dcproject.entity.Vote as VoteEntity
class VoteVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
CREATE,
VIEW
class VoteVoter : Voter() {
fun <S> canCreate(subject: VoteForUpdateI<S, *>, citizen: CitizenI?): VoterResponse where S : EntityDeletedAt, S : TargetI = when {
citizen == null -> denied("You must be connected for vote", "vote.create.connected")
subject.target.isDeleted() -> denied("You cannot vote on deleted target", "vote.create.isDeleted")
else -> granted()
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if ((action is Action && subject == null)) throw NoSubjectDefinedException(action)
if (!(action is Action && subject is VoteI)) return abstain()
fun <S : VoteEntity<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) }
val citizen = context.citizenOrNull ?: return denied("You must be connected for vote", "vote.connected")
if (action == Action.CREATE) {
if (subject !is VoteForUpdateI<*, *>) throw NoSubjectDefinedException(action)
subject.target.let {
if (it is EntityDeletedAt) {
if (it.isDeleted()) return denied("You cannot vote on deleted target", "vote.create.isDeleted")
} else {
throw NoSubjectDefinedException(action)
}
}
return granted()
}
if (action == Action.VIEW) {
if (subject is VoteEntity<*>) {
return if (subject.createdBy.id != citizen.id) {
denied("You can view only your votes", "vote.view")
} else {
granted()
}
} else {
throw NoSubjectDefinedException(action)
}
}
return abstain()
fun canView(subject: VoteEntity<*>, citizen: CitizenI?): VoterResponse = when {
citizen == null -> denied("You must be connected for view your votes", "vote.view.connected")
subject.createdBy.id != citizen.id -> denied("You can only display your votes", "vote.view.onlyYours")
else -> granted()
}
}

View File

@@ -4,28 +4,21 @@ import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.auth.User
import fr.dcproject.component.auth.UserI
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.security.voter.VoteVoter
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.can
import fr.ktorVoter.canAll
import fr.dcproject.voter.Vote.DENIED
import fr.dcproject.voter.Vote.GRANTED
import io.ktor.application.*
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import org.amshove.kluent.`should be`
import org.joda.time.DateTime
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
import java.util.*
@@ -120,101 +113,44 @@ internal class VoteVoterTest {
}
@Test
fun `support vote`(): Unit = VoteVoter().run {
val p = object : ActionI {}
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
this(VoteVoter.Action.VIEW, it, vote1).vote `should be` Vote.GRANTED
this(VoteVoter.Action.VIEW, it, article1).vote `should be` Vote.ABSTAIN
this(p, it, vote1).vote `should be` Vote.ABSTAIN
}
fun `can be view your the vote`() {
VoteVoter()
.canView(vote1, tesla)
.vote `should be` GRANTED
}
@Test
fun `can be view your the vote`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
can(VoteVoter.Action.VIEW, it, vote1) `should be` true
}
fun `can not be view vote of other`() {
VoteVoter()
.canView(vote1, einstein)
.vote `should be` DENIED
}
@Test
fun `can not be view vote of other`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns einstein
}.let {
can(VoteVoter.Action.VIEW, it, vote1) `should be` false
}
}
@Test
fun `can be not view the vote if is null`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
assertThrows<NoSubjectDefinedException> {
can(VoteVoter.Action.VIEW, it, null)
}
}
}
@Test
fun `can be view your votes list`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
canAll(VoteVoter.Action.VIEW, it, listOf(vote1)) `should be` true
}
fun `can be view your votes list`() {
VoteVoter()
.canView(listOf(vote1), tesla)
.vote `should be` GRANTED
}
@Test
fun `can be vote an article`() {
listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
can(VoteVoter.Action.CREATE, it, voteForUpdate) `should be` true
}
}
VoteVoter()
.canCreate(voteForUpdate, tesla)
.vote `should be` GRANTED
}
@Test
fun `can not be vote if not connected`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns null
}.let {
can(VoteVoter.Action.CREATE, it, voteForUpdate) `should be` false
}
fun `can not be vote if not connected`() {
VoteVoter()
.canCreate(voteForUpdate, null)
.vote `should be` DENIED
}
@Test
fun `can not be vote an article if article is deleted`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
can(VoteVoter.Action.CREATE, it, voteOnDeleted) `should be` false
}
}
@Test
fun `can not be vote an article if article have no user`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
assertThrows<NoSubjectDefinedException> {
can(VoteVoter.Action.CREATE, it, voteWithoutTargetUser)
}
}
}
@Test
fun `can not be comment an article if article is deleted`(): Unit = listOf(VoteVoter()).run {
mockk<ApplicationCall> {
every { citizenOrNull } returns tesla
}.let {
can(VoteVoter.Action.CREATE, it, voteOnDeleted) `should be` false
}
fun `can not be vote an article if article is deleted`() {
VoteVoter()
.canCreate(voteOnDeleted, tesla)
.vote `should be` DENIED
}
}