Refactoring of VoteVoter
This commit is contained in:
@@ -96,7 +96,6 @@ fun Application.module(env: Env = PROD) {
|
|||||||
|
|
||||||
install(AuthorizationVoter) {
|
install(AuthorizationVoter) {
|
||||||
voters = listOf(
|
voters = listOf(
|
||||||
VoteVoter(),
|
|
||||||
FollowVoter(),
|
FollowVoter(),
|
||||||
OpinionVoter(),
|
OpinionVoter(),
|
||||||
OpinionChoiceVoter()
|
OpinionChoiceVoter()
|
||||||
@@ -211,8 +210,8 @@ fun Application.module(env: Env = PROD) {
|
|||||||
followArticle(get())
|
followArticle(get())
|
||||||
followConstitution(get())
|
followConstitution(get())
|
||||||
commentConstitution(get(), get())
|
commentConstitution(get(), get())
|
||||||
voteArticle(get(), get(), get())
|
voteArticle(get(), get(), get(), get())
|
||||||
voteConstitution(get())
|
voteConstitution(get(), get())
|
||||||
opinionArticle(get())
|
opinionArticle(get())
|
||||||
opinionChoice(get())
|
opinionChoice(get())
|
||||||
definition()
|
definition()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import fr.dcproject.messages.Mailer
|
|||||||
import fr.dcproject.messages.NotificationEmailSender
|
import fr.dcproject.messages.NotificationEmailSender
|
||||||
import fr.dcproject.repository.CommentConstitutionRepository
|
import fr.dcproject.repository.CommentConstitutionRepository
|
||||||
import fr.dcproject.security.voter.ConstitutionVoter
|
import fr.dcproject.security.voter.ConstitutionVoter
|
||||||
|
import fr.dcproject.security.voter.VoteVoter
|
||||||
import fr.postgresjson.connexion.Connection
|
import fr.postgresjson.connexion.Connection
|
||||||
import fr.postgresjson.connexion.Requester
|
import fr.postgresjson.connexion.Requester
|
||||||
import fr.postgresjson.migration.Migrations
|
import fr.postgresjson.migration.Migrations
|
||||||
@@ -125,6 +126,7 @@ val KoinModule = module {
|
|||||||
single { CommentVoter() }
|
single { CommentVoter() }
|
||||||
single { WorkgroupVoter() }
|
single { WorkgroupVoter() }
|
||||||
single { ConstitutionVoter() }
|
single { ConstitutionVoter() }
|
||||||
|
single { VoteVoter() }
|
||||||
|
|
||||||
// Elasticsearch Client
|
// Elasticsearch Client
|
||||||
single<RestClient> {
|
single<RestClient> {
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ package fr.dcproject.routes
|
|||||||
|
|
||||||
import fr.dcproject.component.article.ArticleForView
|
import fr.dcproject.component.article.ArticleForView
|
||||||
import fr.dcproject.component.auth.citizen
|
import fr.dcproject.component.auth.citizen
|
||||||
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
import fr.dcproject.component.citizen.Citizen
|
import fr.dcproject.component.citizen.Citizen
|
||||||
import fr.dcproject.component.comment.generic.CommentRepository
|
import fr.dcproject.component.comment.generic.CommentRepository
|
||||||
import fr.dcproject.entity.VoteForUpdate
|
import fr.dcproject.entity.VoteForUpdate
|
||||||
import fr.dcproject.repository.VoteComment
|
import fr.dcproject.repository.VoteComment
|
||||||
import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest
|
import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest
|
||||||
import fr.dcproject.routes.VoteArticlePaths.CommentVoteRequest
|
import fr.dcproject.routes.VoteArticlePaths.CommentVoteRequest
|
||||||
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
|
import fr.dcproject.security.voter.VoteVoter
|
||||||
import fr.dcproject.security.voter.VoteVoter.Action.VIEW
|
|
||||||
import fr.dcproject.utils.toUUID
|
import fr.dcproject.utils.toUUID
|
||||||
import fr.ktorVoter.assertCan
|
import fr.dcproject.voter.assert
|
||||||
import fr.ktorVoter.assertCanAll
|
|
||||||
import io.ktor.application.*
|
import io.ktor.application.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.locations.*
|
import io.ktor.locations.*
|
||||||
@@ -49,7 +48,7 @@ object VoteArticlePaths {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentRepository) {
|
fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentRepository, voter: VoteVoter) {
|
||||||
put<ArticleVoteRequest> {
|
put<ArticleVoteRequest> {
|
||||||
val content = call.receive<ArticleVoteRequest.Content>()
|
val content = call.receive<ArticleVoteRequest.Content>()
|
||||||
val vote = VoteForUpdate(
|
val vote = VoteForUpdate(
|
||||||
@@ -57,7 +56,7 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
|
|||||||
note = content.note,
|
note = content.note,
|
||||||
createdBy = this.citizen
|
createdBy = this.citizen
|
||||||
)
|
)
|
||||||
assertCan(CREATE, vote)
|
voter.assert { canCreate(vote, citizenOrNull) }
|
||||||
val votes = repo.vote(vote)
|
val votes = repo.vote(vote)
|
||||||
call.respond(HttpStatusCode.Created, votes)
|
call.respond(HttpStatusCode.Created, votes)
|
||||||
}
|
}
|
||||||
@@ -70,14 +69,14 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
|
|||||||
note = content.note,
|
note = content.note,
|
||||||
createdBy = this.citizen
|
createdBy = this.citizen
|
||||||
)
|
)
|
||||||
assertCan(CREATE, vote)
|
voter.assert { canCreate(vote, citizenOrNull) }
|
||||||
val votes = voteCommentRepo.vote(vote)
|
val votes = voteCommentRepo.vote(vote)
|
||||||
call.respond(HttpStatusCode.Created, votes)
|
call.respond(HttpStatusCode.Created, votes)
|
||||||
}
|
}
|
||||||
|
|
||||||
get<VoteArticlePaths.CitizenVoteArticleRequest> {
|
get<VoteArticlePaths.CitizenVoteArticleRequest> {
|
||||||
val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
|
val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
|
||||||
assertCanAll(VIEW, votes.result)
|
voter.assert { canView(votes.result, citizenOrNull) }
|
||||||
|
|
||||||
call.respond(votes)
|
call.respond(votes)
|
||||||
}
|
}
|
||||||
@@ -85,7 +84,7 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
|
|||||||
get<VoteArticlePaths.CitizenVotesByIdsRequest> {
|
get<VoteArticlePaths.CitizenVotesByIdsRequest> {
|
||||||
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
|
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
|
||||||
if (votes.isNotEmpty()) {
|
if (votes.isNotEmpty()) {
|
||||||
assertCanAll(VIEW, votes)
|
voter.assert { canView(votes, citizenOrNull) }
|
||||||
}
|
}
|
||||||
call.respond(votes)
|
call.respond(votes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package fr.dcproject.routes
|
package fr.dcproject.routes
|
||||||
|
|
||||||
import fr.dcproject.component.auth.citizen
|
import fr.dcproject.component.auth.citizen
|
||||||
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
import fr.dcproject.component.citizen.Citizen
|
import fr.dcproject.component.citizen.Citizen
|
||||||
import fr.dcproject.entity.VoteForUpdate
|
import fr.dcproject.entity.VoteForUpdate
|
||||||
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
|
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
|
||||||
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
|
import fr.dcproject.security.voter.VoteVoter
|
||||||
import fr.ktorVoter.assertCan
|
import fr.dcproject.voter.assert
|
||||||
import io.ktor.application.*
|
import io.ktor.application.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.locations.*
|
import io.ktor.locations.*
|
||||||
@@ -27,7 +28,7 @@ object VoteConstitutionPaths {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
fun Route.voteConstitution(repo: VoteConstitutionRepository) {
|
fun Route.voteConstitution(repo: VoteConstitutionRepository, voter: VoteVoter) {
|
||||||
put<VoteConstitutionPaths.ConstitutionVoteRequest> {
|
put<VoteConstitutionPaths.ConstitutionVoteRequest> {
|
||||||
val content = call.receive<Content>()
|
val content = call.receive<Content>()
|
||||||
val vote = VoteForUpdate(
|
val vote = VoteForUpdate(
|
||||||
@@ -35,7 +36,7 @@ fun Route.voteConstitution(repo: VoteConstitutionRepository) {
|
|||||||
note = content.note,
|
note = content.note,
|
||||||
createdBy = this.citizen
|
createdBy = this.citizen
|
||||||
)
|
)
|
||||||
assertCan(CREATE, vote)
|
voter.assert { canCreate(vote, citizenOrNull) }
|
||||||
repo.vote(vote)
|
repo.vote(vote)
|
||||||
call.respond(HttpStatusCode.Created)
|
call.respond(HttpStatusCode.Created)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,26 @@
|
|||||||
package fr.dcproject.security.voter
|
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.VoteForUpdateI
|
||||||
import fr.dcproject.entity.VoteI
|
import fr.dcproject.voter.Voter
|
||||||
import fr.dcproject.voter.NoSubjectDefinedException
|
import fr.dcproject.voter.VoterResponse
|
||||||
import fr.ktorVoter.*
|
|
||||||
import fr.postgresjson.entity.EntityDeletedAt
|
import fr.postgresjson.entity.EntityDeletedAt
|
||||||
import io.ktor.application.*
|
|
||||||
import fr.dcproject.entity.Vote as VoteEntity
|
import fr.dcproject.entity.Vote as VoteEntity
|
||||||
|
|
||||||
class VoteVoter : Voter<ApplicationCall> {
|
class VoteVoter : Voter() {
|
||||||
enum class Action : ActionI {
|
fun <S> canCreate(subject: VoteForUpdateI<S, *>, citizen: CitizenI?): VoterResponse where S : EntityDeletedAt, S : TargetI = when {
|
||||||
CREATE,
|
citizen == null -> denied("You must be connected for vote", "vote.create.connected")
|
||||||
VIEW
|
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 {
|
fun <S : VoteEntity<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse =
|
||||||
if ((action is Action && subject == null)) throw NoSubjectDefinedException(action)
|
canAll(subjects) { canView(it, citizen) }
|
||||||
if (!(action is Action && subject is VoteI)) return abstain()
|
|
||||||
|
|
||||||
val citizen = context.citizenOrNull ?: return denied("You must be connected for vote", "vote.connected")
|
fun canView(subject: VoteEntity<*>, citizen: CitizenI?): VoterResponse = when {
|
||||||
|
citizen == null -> denied("You must be connected for view your votes", "vote.view.connected")
|
||||||
if (action == Action.CREATE) {
|
subject.createdBy.id != citizen.id -> denied("You can only display your votes", "vote.view.onlyYours")
|
||||||
if (subject !is VoteForUpdateI<*, *>) throw NoSubjectDefinedException(action)
|
else -> granted()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,28 +4,21 @@ import fr.dcproject.component.article.ArticleForView
|
|||||||
import fr.dcproject.component.article.ArticleRef
|
import fr.dcproject.component.article.ArticleRef
|
||||||
import fr.dcproject.component.auth.User
|
import fr.dcproject.component.auth.User
|
||||||
import fr.dcproject.component.auth.UserI
|
import fr.dcproject.component.auth.UserI
|
||||||
import fr.dcproject.component.auth.citizenOrNull
|
|
||||||
import fr.dcproject.component.citizen.Citizen
|
import fr.dcproject.component.citizen.Citizen
|
||||||
import fr.dcproject.component.citizen.CitizenBasic
|
import fr.dcproject.component.citizen.CitizenBasic
|
||||||
import fr.dcproject.component.citizen.CitizenCart
|
import fr.dcproject.component.citizen.CitizenCart
|
||||||
import fr.dcproject.component.citizen.CitizenI
|
import fr.dcproject.component.citizen.CitizenI
|
||||||
import fr.dcproject.entity.VoteForUpdate
|
import fr.dcproject.entity.VoteForUpdate
|
||||||
import fr.dcproject.security.voter.VoteVoter
|
import fr.dcproject.security.voter.VoteVoter
|
||||||
import fr.dcproject.voter.NoSubjectDefinedException
|
import fr.dcproject.voter.Vote.DENIED
|
||||||
import fr.ktorVoter.ActionI
|
import fr.dcproject.voter.Vote.GRANTED
|
||||||
import fr.ktorVoter.Vote
|
|
||||||
import fr.ktorVoter.can
|
|
||||||
import fr.ktorVoter.canAll
|
|
||||||
import io.ktor.application.*
|
import io.ktor.application.*
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
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.Execution
|
||||||
import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
|
import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -120,101 +113,44 @@ internal class VoteVoterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `support vote`(): Unit = VoteVoter().run {
|
fun `can be view your the vote`() {
|
||||||
val p = object : ActionI {}
|
VoteVoter()
|
||||||
mockk<ApplicationCall> {
|
.canView(vote1, tesla)
|
||||||
every { citizenOrNull } returns tesla
|
.vote `should be` GRANTED
|
||||||
}.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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can be view your the vote`(): Unit = listOf(VoteVoter()).run {
|
fun `can not be view vote of other`() {
|
||||||
mockk<ApplicationCall> {
|
VoteVoter()
|
||||||
every { citizenOrNull } returns tesla
|
.canView(vote1, einstein)
|
||||||
}.let {
|
.vote `should be` DENIED
|
||||||
can(VoteVoter.Action.VIEW, it, vote1) `should be` true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can not be view vote of other`(): Unit = listOf(VoteVoter()).run {
|
fun `can be view your votes list`() {
|
||||||
mockk<ApplicationCall> {
|
VoteVoter()
|
||||||
every { citizenOrNull } returns einstein
|
.canView(listOf(vote1), tesla)
|
||||||
}.let {
|
.vote `should be` GRANTED
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can be vote an article`() {
|
fun `can be vote an article`() {
|
||||||
listOf(VoteVoter()).run {
|
VoteVoter()
|
||||||
mockk<ApplicationCall> {
|
.canCreate(voteForUpdate, tesla)
|
||||||
every { citizenOrNull } returns tesla
|
.vote `should be` GRANTED
|
||||||
}.let {
|
|
||||||
can(VoteVoter.Action.CREATE, it, voteForUpdate) `should be` true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can not be vote if not connected`(): Unit = listOf(VoteVoter()).run {
|
fun `can not be vote if not connected`() {
|
||||||
mockk<ApplicationCall> {
|
VoteVoter()
|
||||||
every { citizenOrNull } returns null
|
.canCreate(voteForUpdate, null)
|
||||||
}.let {
|
.vote `should be` DENIED
|
||||||
can(VoteVoter.Action.CREATE, it, voteForUpdate) `should be` false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can not be vote an article if article is deleted`(): Unit = listOf(VoteVoter()).run {
|
fun `can not be vote an article if article is deleted`() {
|
||||||
mockk<ApplicationCall> {
|
VoteVoter()
|
||||||
every { citizenOrNull } returns tesla
|
.canCreate(voteOnDeleted, tesla)
|
||||||
}.let {
|
.vote `should be` DENIED
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user