Add Tests

This commit is contained in:
2021-06-26 01:51:33 +02:00
parent 181176ed21
commit ce55c58430
7 changed files with 236 additions and 25 deletions

View File

@@ -53,6 +53,19 @@ jobs:
gradle-version: '7.1' gradle-version: '7.1'
arguments: test arguments: test
- name: Coverage
uses: eskatos/gradle-command-action@v1
with:
gradle-version: '7.1'
arguments: jacocoTestReport
- name: Cache SonarCloud packages
uses: actions/cache@v1
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Push analyse to sonarqube - name: Push analyse to sonarqube
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
env: env:

8
README.md Normal file
View File

@@ -0,0 +1,8 @@
# access-control
Helpers to create Access Control
[![Tests](https://github.com/flecomte/access-control/actions/workflows/tests.yml/badge.svg)](https://github.com/flecomte/access-control/actions/workflows/tests.yml)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=flecomte_access-control&metric=coverage)](https://sonarcloud.io/dashboard?id=flecomte_access-control)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=flecomte_access-control&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=flecomte_access-control)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=flecomte_access-control&metric=ncloc)](https://sonarcloud.io/dashboard?id=flecomte_access-control)

View File

@@ -3,6 +3,7 @@ plugins {
`maven-publish` `maven-publish`
kotlin("jvm") version "+" kotlin("jvm") version "+"
id("net.nemerosa.versioning") version "+" id("net.nemerosa.versioning") version "+"
id("org.jlleitschuh.gradle.ktlint") version "+"
id("org.sonarqube") version "+" id("org.sonarqube") version "+"
} }
@@ -15,6 +16,14 @@ version = versioning.info.run {
} }
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
sourceCompatibility = "11"
targetCompatibility = "11"
}
}
tasks.jacocoTestReport { tasks.jacocoTestReport {
dependsOn(tasks.test) dependsOn(tasks.test)
reports { reports {
@@ -61,3 +70,11 @@ publishing {
repositories { repositories {
mavenCentral() mavenCentral()
} }
tasks.test {
useJUnitPlatform()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.7.+")
}

View File

@@ -1,6 +1,6 @@
kotlin.code.style=official kotlin.code.style=official
systemProp.sonar.host.url=https://sonarcloud.io systemProp.sonar.host.url=https://sonarcloud.io
systemProp.sonar.projectKey=AccessControl systemProp.sonar.projectKey=flecomte_access-control
systemProp.sonar.projectName=AccessControl systemProp.sonar.projectName=AccessControl
systemProp.sonar.organization=flecomte systemProp.sonar.organization=flecomte
systemProp.sonar.java.coveragePlugin=jacoco systemProp.sonar.java.coveragePlugin=jacoco

View File

@@ -1,2 +1 @@
rootProject.name = "access-control" rootProject.name = "access-control"

View File

@@ -1,4 +1,4 @@
package fr.dcproject.common.security package io.github.flecomte
/** Responses of AccessControl */ /** Responses of AccessControl */
enum class AccessDecision { enum class AccessDecision {
@@ -25,19 +25,17 @@ abstract class AccessControl {
*/ */
protected fun denied(message: String, code: String): DeniedResponse = DeniedResponse(this, message, code) protected fun denied(message: String, code: String): DeniedResponse = DeniedResponse(this, message, code)
/**
* Check all responses and return DENIED if one is DENIED
*
* If the list of responses is empty, return GRANTED
*/
private fun AccessResponses.getOneResponse(): AccessResponse = this.firstOrNull { it.decision == AccessDecision.DENIED } ?: granted()
/** /**
* A helper to convert a list of subject into one response * A helper to convert a list of subject into one response
*/ */
protected fun <S : List<T>, T> canAll(items: S, action: (T) -> AccessResponse): AccessResponse = items protected fun <S : List<T>, T> canAll(items: S, action: (T) -> AccessResponse): AccessResponses = items
.map { action(it) } .map { action(it) }.let { responses ->
.getOneResponse() if (responses.any { it is DeniedResponse }) {
DeniedResponses(responses)
} else {
GrantedResponses(responses)
}
}
} }
/** /**
@@ -47,49 +45,65 @@ fun <T : AccessControl> T.assert(action: T.() -> AccessResponse) {
action().assert() action().assert()
} }
typealias AccessResponses = List<AccessResponse>
/** /**
* Check all responses and return DENIED if one is DENIED * Check all responses and return DENIED if one is DENIED
* *
* If the list of responses is empty, return GRANTED * If the list of responses is empty, return GRANTED
*/ */
fun AccessResponses.getOneResponse(): AccessResponse = this.firstOrNull { it.decision == AccessDecision.DENIED } ?: GrantedResponse(first().accessControl) fun AccessResponses.getFirstDecisionResponse(): AccessResponse = this.firstOrNull { it.decision == AccessDecision.DENIED } ?: this.first { it.decision == AccessDecision.GRANTED }
/** /**
* Throw an Exception if one response is DENIED * Throw an Exception if one response is DENIED
*/ */
fun AccessResponses.assert() = this.getOneResponse().assert() fun AccessResponses.assert() {
if (!toBoolean()) {
throw AccessDeniedException(this)
}
}
val AccessResponses.grantedResponses get(): AccessResponses = this.filterIsInstance<GrantedResponse>()
val AccessResponses.deniedResponses get(): AccessResponses = this.filterIsInstance<DeniedResponse>()
class AccessDeniedException(private val accessResponses: AccessResponses) : Throwable(accessResponses.first().message) { /**
* Convert responses as boolean
*/
fun AccessResponses.toBoolean(): Boolean = deniedResponses.isEmpty()
class AccessDeniedException(val accessResponses: AccessResponses) : Throwable(accessResponses.deniedResponses.first().message) {
constructor(accessResponse: AccessResponse) : this(listOf(accessResponse)) constructor(accessResponse: AccessResponse) : this(listOf(accessResponse))
/** /**
* Get first response * Get first response
*/ */
fun first(): AccessResponse = accessResponses.first() fun first(): AccessResponse = accessResponses.deniedResponses.first()
/** /**
* Check if the error code is present into the responses * Check if the error code is present into the responses
*/ */
fun hasErrorCode(code: String): Boolean = accessResponses fun hasErrorCode(code: String): Boolean = accessResponses
.filter { it.decision == AccessDecision.DENIED } .deniedResponses
.any { it.code == code } .any { it.code == code }
/** /**
* Find and return the response than match with the error code * Find and return the response than match with the error code
*/ */
fun getErrorCode(code: String): AccessResponse? = accessResponses fun getErrorCode(code: String): AccessResponse? = accessResponses
.firstOrNull { it.decision == AccessDecision.DENIED && it.code == code } .deniedResponses
.firstOrNull { it.code == code }
/** /**
* Get a list of messages of all responses * Get a list of messages of all responses
*/ */
fun getMessages(): List<String> = accessResponses fun getMessages(): List<String> = accessResponses
.mapNotNull { it.message } .deniedResponses
.map { it.message!! }
/** /**
* Get the first message * Get the first message
*/ */
fun getFirstMessage(): String? = accessResponses fun getFirstMessage(): String? = accessResponses
.deniedResponses
.first() .first()
.message .message
} }
@@ -120,16 +134,32 @@ sealed class AccessResponse(
} }
} }
class GrantedResponse( open class GrantedResponse(
accessControl: AccessControl, accessControl: AccessControl,
message: String? = null, message: String? = null,
code: String? = null code: String? = null
) : AccessResponse(AccessDecision.GRANTED, accessControl, message, code) ) : AccessResponse(AccessDecision.GRANTED, accessControl, message, code)
class DeniedResponse( open class DeniedResponse(
accessControl: AccessControl, accessControl: AccessControl,
message: String, message: String,
code: String code: String
) : AccessResponse(AccessDecision.DENIED, accessControl, message, code) ) : AccessResponse(AccessDecision.DENIED, accessControl, message, code)
typealias AccessResponses = List<AccessResponse> class GrantedResponses(
accessResponses: List<AccessResponse>
) : AccessResponses by accessResponses,
GrantedResponse(
accessResponses.grantedResponses.first().accessControl,
accessResponses.grantedResponses.first().message,
accessResponses.grantedResponses.first().code
)
class DeniedResponses(
accessResponses: List<AccessResponse>
) : AccessResponses by accessResponses,
DeniedResponse(
accessResponses.deniedResponses.first().accessControl,
accessResponses.deniedResponses.firstOrNull()?.message ?: error("DeniedResponses cannot be empty"),
accessResponses.deniedResponses.firstOrNull()?.code ?: error("DeniedResponses cannot be empty")
)

View File

@@ -0,0 +1,144 @@
package io.github.flecomte
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
data class User(
val name: String
)
data class MyObject(
val title: String
)
class AccessControlSample : AccessControl() {
fun canView(myObject: MyObject, user: User?): AccessResponse {
return if (myObject.title == "granted" && user != null) {
granted("ok")
} else if (myObject.title == "wrong2") {
denied("KO2", "ko2")
} else {
denied("KO", "ko")
}
}
fun canView(myObjects: List<MyObject>, user: User?): AccessResponses {
return canAll(myObjects) { canView(it, user) }
}
}
class AccessControlTest {
@Test
fun `test granted`() {
AccessControlSample().run {
assertTrue(canView(MyObject("granted"), User("")).toBoolean())
}
}
@Test
fun `test denied`() {
AccessControlSample().run {
assertFalse(canView(MyObject("wrong"), User("")).toBoolean())
}
}
@Test
fun `test canAllGranted`() {
AccessControlSample().run {
assertTrue(
canView(
listOf(
MyObject("granted"),
MyObject("granted")
),
User("")
).first().toBoolean()
)
}
}
@Test
fun `test CanAllDenied`() {
AccessControlSample().run {
assertFalse(
canView(
listOf(
MyObject("granted"),
MyObject("wrong")
),
User("")
).toBoolean()
)
}
}
@Test
fun `test Assert on fail`() {
assertThrows(AccessDeniedException::class.java) {
AccessControlSample().canView(MyObject("denied"), User("")).assert()
}
}
@Test
fun `test Assert on success`() {
AccessControlSample().assert { canView(MyObject("granted"), User("")) }
}
@Test
fun `Exception tests`() {
assertThrows(AccessDeniedException::class.java) {
AccessControlSample().canView(listOf(MyObject("wrong"), MyObject("granted"), MyObject("wrong2")), User("")).assert()
}.run {
assertEquals("ko", first().code)
assertTrue(hasErrorCode("ko"))
assertFalse(hasErrorCode("notExists"))
assertEquals("ko", getErrorCode("ko")?.code)
assertEquals(null, getErrorCode("notExists")?.code)
assertEquals("KO2", getMessages().last())
assertEquals("KO", getFirstMessage())
}
}
@Test
fun `Assert success`() {
AccessControlSample()
.canView(listOf(MyObject("granted"), MyObject("granted")), User(""))
.assert()
}
@Test
fun `test getFirstDecisionResponse`() {
AccessControlSample()
.canView(listOf(MyObject("granted"), MyObject("granted")), User(""))
.getFirstDecisionResponse()
.run {
assertTrue(decision.toBoolean())
}
}
@Test
fun `test getFirstDecisionResponse on denied`() {
AccessControlSample()
.canView(listOf(MyObject("granted"), MyObject("denied")), User(""))
.getFirstDecisionResponse()
.run {
assertFalse(decision.toBoolean())
}
}
@Test
fun `GrantedResponse instantiation test`() {
assertEquals(AccessDecision.GRANTED, GrantedResponse(AccessControlSample()).decision)
}
@Test
fun `DeniedResponses must be throw exception if have no denied responses`() {
assertThrows(Exception::class.java) {
DeniedResponses(
listOf(GrantedResponse(AccessControlSample())),
)
}
}
}