#42 Add tests to ArticleVoter

Refactor ArticleVoter
This commit is contained in:
2020-03-16 03:46:31 +01:00
parent aa7ca26b51
commit ca78db4155
8 changed files with 271 additions and 87 deletions

View File

@@ -97,7 +97,7 @@ dependencies {
testImplementation("io.ktor:ktor-client-mock:$ktor_version") testImplementation("io.ktor:ktor-client-mock:$ktor_version")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktor_version") testImplementation("io.ktor:ktor-client-mock-jvm:$ktor_version")
testImplementation("org.koin:koin-test:$koinVersion") testImplementation("org.koin:koin-test:$koinVersion")
testImplementation("io.mockk:mockk:1.9") testImplementation("io.mockk:mockk:1.9.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.5.0") testImplementation("org.junit.jupiter:junit-jupiter:5.5.0")
testImplementation("org.amshove.kluent:kluent:1.4") testImplementation("org.amshove.kluent:kluent:1.4")
testImplementation("io.cucumber:cucumber-java8:$cucumber_version") testImplementation("io.cucumber:cucumber-java8:$cucumber_version")

View File

@@ -8,30 +8,19 @@ import fr.postgresjson.entity.mutable.UuidEntityVersioning
import java.util.* import java.util.*
class Article( class Article(
id: UUID = UUID.randomUUID(),
title: String,
anonymous: Boolean = true,
content: String,
description: String,
tags: List<String> = emptyList(),
override var draft: Boolean = false,
override var lastVersion: Boolean = false,
createdBy: CitizenBasic
) : ArticleFull,
ArticleBasic(id, title, anonymous, content, description, tags, createdBy),
Viewable by ViewableImp()
open class ArticleBasic(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
title: String, title: String,
override var anonymous: Boolean = true, override var anonymous: Boolean = true,
override var content: String, override var content: String,
override var description: String, override var description: String,
override var tags: List<String> = emptyList(), override var tags: List<String> = emptyList(),
draft: Boolean = false,
override var lastVersion: Boolean = false,
override val createdBy: CitizenBasic override val createdBy: CitizenBasic
) : ArticleBasicI, ) : ArticleFull,
ArticleSimple(id, title, createdBy) { ArticleAuthI<CitizenBasicI>,
ArticleSimple(id, title, createdBy, draft),
Viewable by ViewableImp() {
init { init {
tags = tags.distinct() tags = tags.distinct()
} }
@@ -40,8 +29,10 @@ open class ArticleBasic(
open class ArticleSimple( open class ArticleSimple(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override var title: String, override var title: String,
override val createdBy: CitizenBasic override val createdBy: CitizenBasic,
override var draft: Boolean = false
) : ArticleSimpleI, ) : ArticleSimpleI,
ArticleAuthI<CitizenBasicI>,
ArticleRefVersioning(id), ArticleRefVersioning(id),
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy), EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
@@ -85,3 +76,10 @@ interface ArticleFull :
var draft: Boolean var draft: Boolean
var lastVersion: Boolean var lastVersion: Boolean
} }
interface ArticleAuthI<U : CitizenWithUserI> :
ArticleI,
EntityCreatedBy<U>,
EntityDeletedAt {
var draft: Boolean
}

View File

@@ -29,7 +29,7 @@ open class CitizenBasic(
override var birthday: DateTime, override var birthday: DateTime,
override var voteAnonymous: Boolean = true, override var voteAnonymous: Boolean = true,
override var followAnonymous: Boolean = true, override var followAnonymous: Boolean = true,
user: UserRef override val user: User
) : CitizenBasicI, ) : CitizenBasicI,
CitizenSimple(id, name, user) CitizenSimple(id, name, user)

View File

@@ -10,7 +10,7 @@ class User(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
username: String, username: String,
blockedAt: DateTime? = null, blockedAt: DateTime? = null,
override var plainPassword: String?, override var plainPassword: String? = null,
override var roles: List<Roles> = emptyList() override var roles: List<Roles> = emptyList()
) : UserFull, UserBasic(id, username, blockedAt), ) : UserFull, UserBasic(id, username, blockedAt),
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),

View File

@@ -1,5 +1,7 @@
package fr.dcproject.security.voter package fr.dcproject.security.voter
import fr.dcproject.entity.ArticleAuthI
import fr.dcproject.entity.ArticleI
import fr.dcproject.entity.ArticleSimpleI import fr.dcproject.entity.ArticleSimpleI
import fr.dcproject.entity.UserI import fr.dcproject.entity.UserI
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
@@ -16,53 +18,52 @@ class ArticleVoter : Voter {
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean { override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return (action is Action || action is CommentVoter.Action || action is VoteVoter.Action) return (action is Action || action is CommentVoter.Action || action is VoteVoter.Action)
.and(subject is List<*> || subject is ArticleSimpleI? || subject is VoteEntity<*> || subject is CommentEntity<*>) .and(subject is ArticleI? || subject is VoteEntity<*> || subject is CommentEntity<*>)
} }
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote { override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
val user = call.user val user = call.user
if (action == Action.CREATE && user is UserI) { if (action == Action.CREATE && user is UserI) return Vote.GRANTED
return Vote.GRANTED if (action == Action.VIEW) return view(subject, user)
} if (action == Action.DELETE) return delete(subject, user)
if (action == Action.UPDATE) return update(subject, user)
if (action == Action.VIEW) {
if (subject is ArticleSimpleI) {
return if (subject.isDeleted()) Vote.DENIED
else Vote.GRANTED
}
if (subject is List<*>) {
subject.forEach {
if (it !is ArticleSimpleI || it.isDeleted()) {
return Vote.DENIED
}
}
return Vote.GRANTED
}
return Vote.DENIED
}
if (action is CommentVoter.Action) return voteForComment(action) if (action is CommentVoter.Action) return voteForComment(action)
if (action is VoteVoter.Action) return voteForVote(action, subject) if (action is VoteVoter.Action) return voteForVote(action, subject)
if (action is Action) return Vote.DENIED
if (subject is ArticleSimpleI) {
if (action == Action.DELETE && user is UserI && subject.createdBy.user.id == user.id) {
return Vote.GRANTED
}
if (action == Action.UPDATE && user is UserI && subject.createdBy.user.id == user.id) {
return Vote.GRANTED
}
return Vote.DENIED
}
if (action is Action) {
return Vote.DENIED
}
return Vote.ABSTAIN return Vote.ABSTAIN
} }
private fun view(subject: Any?, user: UserI?): Vote {
checkClass(ArticleAuthI::class, subject)
if (subject is ArticleAuthI<*>) {
return if (subject.isDeleted()) Vote.DENIED
else if (subject.draft && (user == null || subject.createdBy.user.id != user.id)) Vote.DENIED
else Vote.GRANTED
}
return Vote.DENIED
}
private fun delete(subject: Any?, user: UserI?): Vote {
checkClass(ArticleAuthI::class, subject)
if (subject is ArticleAuthI<*>) {
if (user is UserI && subject.createdBy.user.id == user.id) {
return Vote.GRANTED
}
}
return Vote.DENIED
}
private fun update(subject: Any?, user: UserI?): Vote {
checkClass(ArticleAuthI::class, subject)
if (subject is ArticleAuthI<*>) {
if (user is UserI && subject.createdBy.user.id == user.id) {
return Vote.GRANTED
}
}
return Vote.DENIED
}
private fun voteForVote(action: VoteVoter.Action, subject: Any?): Vote { private fun voteForVote(action: VoteVoter.Action, subject: Any?): Vote {
if (action == VoteVoter.Action.CREATE && subject is VoteEntity<*>) { if (action == VoteVoter.Action.CREATE && subject is VoteEntity<*>) {
val target = subject.target val target = subject.target

View File

@@ -14,7 +14,7 @@ begin
from ( from (
select select
z.*, z.*,
json_build_object('id', z.user_id) as "user" find_user_by_id(z.user_id) as "user"
from citizen as z from citizen as z
where "search" is null or ( where "search" is null or (
(name->'first_name')::text ilike '%'||"search"||'%' or (name->'first_name')::text ilike '%'||"search"||'%' or

View File

@@ -21,37 +21,57 @@ class ArticleTest {
@Language("JSON") @Language("JSON")
private val articleJson: String = """ private val articleJson: String = """
{ {
"id" : "83b0b60a-5ab3-44f2-b243-1dc469a7564f", "id": "83b0b60a-5ab3-44f2-b243-1dc469a7564f",
"version_id" : "fff2311c-07cc-43a6-bab1-aec6b649a903", "title": "Hello world!",
"version_number" : null, "anonymous": true,
"title" : "Hello world!", "content": "bla bla bla",
"anonymous" : true, "description": "this is the changement !",
"content" : "bla bla bla", "tags": [],
"description" : "this is the changement !", "draft": false,
"tags" : [ ], "last_version": false,
"created_by" : { "created_by": {
"id" : "3fff09e4-5ff2-46ee-9fd2-3803a1ffb600", "id": "94a0d350-7eab-4a6e-9f84-0c2e7635b67c",
"name" : { "name": {
"first_name" : "Jaque", "first_name": "Jaque",
"last_name" : "Bono", "last_name": "Bono",
"civility" : null "civility": null
},
"birthday" : "2019-08-03T13:43:13.765Z",
"user_id" : null,
"vote_anonymous" : null,
"follow_anonymous" : null,
"user" : {
"id" : "151ec430-3aad-4792-9a14-e394b2be491b",
"username" : "jaque",
"blocked_at" : null,
"plain_password" : "azerty",
"created_at" : null,
"updated_at" : null
}, },
"email": "jaque.bono@gmail.com", "email": "jaque.bono@gmail.com",
"created_at" : null "birthday": "2020-03-16T01:48:27.020Z",
"vote_anonymous": true,
"follow_anonymous": true,
"user": {
"id": "2bc356a2-4d3e-46ff-91f4-ae30fb7fa67d",
"username": "jaque",
"blocked_at": null,
"plain_password": "azerty",
"roles": [],
"created_at": "2020-03-16T01:48:24.153Z",
"updated_at": "2020-03-16T01:48:24.516Z"
}, },
"created_at" : null "deleted_at": null,
"deleted": false
},
"reference": "article",
"views": {
"total": 0,
"unique": 0,
"updated_at": "2020-03-16T01:48:31.070Z"
},
"version_id": "27cb4f5d-d425-4e10-95ca-6c50fac73408",
"version_number": null,
"created_at": "2020-03-16T01:48:31.004Z",
"deleted_at": null,
"deleted": false,
"votes": {
"up": 0,
"neutral": 0,
"down": 0,
"total": 0,
"score": 0,
"updated_at": null
},
"opinions": {}
} }
""".trimIndent() """.trimIndent()

View File

@@ -0,0 +1,165 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.*
import io.ktor.application.ApplicationCall
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.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ArticleVoterTest {
val tesla = CitizenBasic(
user = User(
username = "nicolas-tesla",
roles = listOf(UserI.Roles.ROLE_USER)
),
birthday = DateTime.now(),
email = "tesla@best.com",
name = CitizenI.Name("Nicolas", "Tesla")
)
val einstein = CitizenBasic(
user = User(
username = "albert-einstein",
roles = listOf(UserI.Roles.ROLE_USER)
),
birthday = DateTime.now(),
email = "einstein@best.com",
name = CitizenI.Name("Albert", "Einstein")
)
init {
mockkStatic("fr.dcproject.security.voter.VoterKt")
}
@Test
fun `creator can be view the article`() = ArticleVoter().run {
val article = getArticle(tesla).apply { draft = true }
mockk<ApplicationCall> {
every { user } returns tesla.user
}.let {
supports(ArticleVoter.Action.VIEW, it, article) `should be` true
vote(ArticleVoter.Action.VIEW, it, article) `should be` Vote.GRANTED
}
}
@Test
fun `other user can be view the article`() = ArticleVoter().run {
val article = getArticle(tesla)
val article2 = getArticle(tesla)
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
supports(ArticleVoter.Action.VIEW, it, article) `should be` true
vote(ArticleVoter.Action.VIEW, it, article) `should be` Vote.GRANTED
}
}
@Test
fun `other user can be view the article list`() = listOf(ArticleVoter()).run {
val article = getArticle(tesla)
val article2 = getArticle(tesla)
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
can(ArticleVoter.Action.VIEW, it, listOf(article, article2)) `should be` true
}
}
@Test
fun `the no creator can not be view the article on draft`() = ArticleVoter().run {
val article = getArticle(tesla).apply { draft = true }
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
supports(ArticleVoter.Action.VIEW, it, article) `should be` true
vote(ArticleVoter.Action.VIEW, it, article) `should be` Vote.DENIED
}
}
@Test
fun `the no creator can not be view list of articles if one is on draft`() = listOf(ArticleVoter()).run {
val article = getArticle(tesla)
val article2 = getArticle(tesla).apply { draft = true }
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
can(ArticleVoter.Action.VIEW, it, listOf(article, article2)) `should be` false
}
}
@Test
fun `can not view deleted article`() = ArticleVoter().run {
val article = getArticle(tesla).apply { deletedAt = DateTime.now() }
mockk<ApplicationCall> {
every { user } returns tesla.user
}.let {
supports(ArticleVoter.Action.VIEW, it, article) `should be` true
vote(ArticleVoter.Action.VIEW, it, article) `should be` Vote.DENIED
}
}
@Test
fun `can delete article if owner`() = ArticleVoter().run {
val article = getArticle(tesla)
mockk<ApplicationCall> {
every { user } returns tesla.user
}.let {
supports(ArticleVoter.Action.DELETE, it, article) `should be` true
vote(ArticleVoter.Action.DELETE, it, article) `should be` Vote.GRANTED
}
}
@Test
fun `can not delete article if not owner`() = ArticleVoter().run {
val article = getArticle(tesla).apply { deletedAt = DateTime.now() }
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
supports(ArticleVoter.Action.DELETE, it, article) `should be` true
vote(ArticleVoter.Action.DELETE, it, article) `should be` Vote.DENIED
}
}
@Test
fun `can create article if logged`() = ArticleVoter().run {
val article = getArticle(tesla)
mockk<ApplicationCall> {
every { user } returns tesla.user
}.let {
supports(ArticleVoter.Action.CREATE, it, article) `should be` true
vote(ArticleVoter.Action.CREATE, it, article) `should be` Vote.GRANTED
}
}
@Test
fun `can not create article if not logged`() = ArticleVoter().run {
val article = getArticle(tesla)
mockk<ApplicationCall> {
every { user } returns null
}.let {
supports(ArticleVoter.Action.CREATE, it, article) `should be` true
vote(ArticleVoter.Action.CREATE, it, article) `should be` Vote.DENIED
}
}
private fun getArticle(createdBy: CitizenBasic = tesla) = Article(
title = "Hello world",
content = "Super",
description = "I Rocks",
createdBy = createdBy
)
}