Add Tests
This commit is contained in:
13
.github/workflows/tests.yml
vendored
13
.github/workflows/tests.yml
vendored
@@ -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
8
README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# access-control
|
||||||
|
Helpers to create Access Control
|
||||||
|
|
||||||
|
[](https://github.com/flecomte/access-control/actions/workflows/tests.yml)
|
||||||
|
[](https://sonarcloud.io/dashboard?id=flecomte_access-control)
|
||||||
|
|
||||||
|
[](https://sonarcloud.io/dashboard?id=flecomte_access-control)
|
||||||
|
[](https://sonarcloud.io/dashboard?id=flecomte_access-control)
|
||||||
@@ -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.+")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
rootProject.name = "access-control"
|
rootProject.name = "access-control"
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
)
|
||||||
144
src/test/kotlin/io/github/flecomte/AccessControlTest.kt
Normal file
144
src/test/kotlin/io/github/flecomte/AccessControlTest.kt
Normal 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())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user