Merge pull request #90 from flecomte/improve-test
Improve tests
This commit was merged in pull request #90.
This commit is contained in:
4
src/main/kotlin/fr/dcproject/common/utils/Numeric.kt
Normal file
4
src/main/kotlin/fr/dcproject/common/utils/Numeric.kt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package fr.dcproject.common.utils
|
||||||
|
|
||||||
|
fun String.isInt(): Boolean = this.toIntOrNull() != null
|
||||||
|
fun String.isBool(): Boolean = this == "true" || this == "false"
|
||||||
7
src/test/kotlin/assert/Range.kt
Normal file
7
src/test/kotlin/assert/Range.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package assert
|
||||||
|
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
infix fun IntProgression.assertContain(expected: Int) {
|
||||||
|
assertTrue(this.contains(expected), "Expected $this less than $expected")
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import org.koin.test.get
|
|||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
@KtorExperimentalAPI
|
@KtorExperimentalAPI
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Tags(Tag("functional"))
|
@Tags(Tag("functional"), Tag("mail"))
|
||||||
class MailerTest : KoinTest, AutoCloseKoinTest() {
|
class MailerTest : KoinTest, AutoCloseKoinTest() {
|
||||||
@InternalCoroutinesApi
|
@InternalCoroutinesApi
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import org.junit.jupiter.api.TestInstance
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
|
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
|
||||||
@Tags(Tag("functional"))
|
@Tags(Tag("functional"), Tag("notification"))
|
||||||
class NotificationConsumerTest {
|
class NotificationConsumerTest {
|
||||||
companion object {
|
companion object {
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import org.junit.jupiter.api.Tags
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@Tags(Tag("functional"))
|
@Tags(Tag("functional"), Tag("notification"))
|
||||||
internal class NotificationsPushTest {
|
internal class NotificationsPushTest {
|
||||||
companion object {
|
companion object {
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import org.junit.jupiter.api.TestInstance
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Tags(Tag("functional"))
|
@Tags(Tag("functional"), Tag("utils"))
|
||||||
class ResourcesKtTest {
|
class ResourcesKtTest {
|
||||||
@Test
|
@Test
|
||||||
fun readResource() {
|
fun readResource() {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import java.util.UUID
|
|||||||
@KtorExperimentalAPI
|
@KtorExperimentalAPI
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@TestInstance(PER_CLASS)
|
@TestInstance(PER_CLASS)
|
||||||
@Tags(Tag("functional"))
|
@Tags(Tag("functional"), Tag("view"))
|
||||||
class ViewTest {
|
class ViewTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test View Article`() {
|
fun `test View Article`() {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class `Article routes` : BaseTest() {
|
|||||||
`And the response should contain pattern`("$.result[1].createdBy.name.firstName", "firstName.+")
|
`And the response should contain pattern`("$.result[1].createdBy.name.firstName", "firstName.+")
|
||||||
`And the response should contain pattern`("$.result[2].createdBy.name.firstName", "firstName.+")
|
`And the response should contain pattern`("$.result[2].createdBy.name.firstName", "firstName.+")
|
||||||
`And the response should not contain`("$.result[3]")
|
`And the response should not contain`("$.result[3]")
|
||||||
`And the response should contain list`("$.result", 3, 3)
|
`And the response should contain list`("$.result", 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class `Comment constitutions routes` : BaseTest() {
|
|||||||
`And the response should contain`("$.limit", 50)
|
`And the response should contain`("$.limit", 50)
|
||||||
`And the response should contain`("$.result[0].createdBy.id", "46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5")
|
`And the response should contain`("$.result[0].createdBy.id", "46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5")
|
||||||
`And the response should contain`("$.result[0].target.id", "34ddd50a-da00-4a90-a869-08baa2a121be")
|
`And the response should contain`("$.result[0].target.id", "34ddd50a-da00-4a90-a869-08baa2a121be")
|
||||||
`And the response should contain list`("$.result[*]", 1, 1)
|
`And the response should contain list`("$.result[*]", 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Tags(Tag("integration"), Tag("article"), Tag("opinion"))
|
@Tags(Tag("integration"), Tag("opinion"))
|
||||||
class `Opinion routes` : BaseTest() {
|
class `Opinion routes` : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `I can get all opinion choices`() {
|
fun `I can get all opinion choices`() {
|
||||||
@@ -48,6 +48,7 @@ class `Opinion routes` : BaseTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Tag("article")
|
||||||
fun `I can create opinion on article`() {
|
fun `I can create opinion on article`() {
|
||||||
withIntegrationApplication {
|
withIntegrationApplication {
|
||||||
`Given I have citizen`("Isaac", "Newton", id = "2f414045-95d9-42ca-a3a9-8cdde52ad253")
|
`Given I have citizen`("Isaac", "Newton", id = "2f414045-95d9-42ca-a3a9-8cdde52ad253")
|
||||||
@@ -89,6 +90,7 @@ class `Opinion routes` : BaseTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Tag("article")
|
||||||
fun `I can receive opinion aggregation with article`() {
|
fun `I can receive opinion aggregation with article`() {
|
||||||
withIntegrationApplication {
|
withIntegrationApplication {
|
||||||
`Given I have an opinion choice`("Opinion6")
|
`Given I have an opinion choice`("Opinion6")
|
||||||
@@ -120,6 +122,7 @@ class `Opinion routes` : BaseTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Tag("article")
|
||||||
fun `I can get all my opinion of one article`() {
|
fun `I can get all my opinion of one article`() {
|
||||||
withIntegrationApplication {
|
withIntegrationApplication {
|
||||||
`Given I have citizen`("Albert", "Einstein", id = "c1542096-3431-432d-8e35-9dc071d4c818")
|
`Given I have citizen`("Albert", "Einstein", id = "c1542096-3431-432d-8e35-9dc071d4c818")
|
||||||
@@ -134,7 +137,7 @@ class `Opinion routes` : BaseTest() {
|
|||||||
`authenticated as`("Albert", "Einstein")
|
`authenticated as`("Albert", "Einstein")
|
||||||
} `Then the response should be` OK and {
|
} `Then the response should be` OK and {
|
||||||
`And the response should contain`("$.result[0].name", "Opinion9")
|
`And the response should contain`("$.result[0].name", "Opinion9")
|
||||||
`And the response should contain list`("$.result[*]", 1, 1)
|
`And the response should contain list`("$.result[*]", 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class `Workgroup routes` : BaseTest() {
|
|||||||
`And the response should contain`("$.description", "Une petite souris")
|
`And the response should contain`("$.description", "Une petite souris")
|
||||||
|
|
||||||
`And have property`("$.members")
|
`And have property`("$.members")
|
||||||
`And the response should contain list`("$.members", 3, 3)
|
`And the response should contain list`("$.members", 3)
|
||||||
`And the response should contain`("$.members.[1]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9")
|
`And the response should contain`("$.members.[1]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9")
|
||||||
`And the response should contain`("$.members.[2]citizen.id", "87909ba3-2069-431c-9924-219fd8411cf2")
|
`And the response should contain`("$.members.[2]citizen.id", "87909ba3-2069-431c-9924-219fd8411cf2")
|
||||||
}
|
}
|
||||||
@@ -215,7 +215,7 @@ class `Workgroup routes` : BaseTest() {
|
|||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
} `Then the response should be` OK and {
|
} `Then the response should be` OK and {
|
||||||
`And the response should contain list`("$", 2, 2)
|
`And the response should contain list`("$", 2)
|
||||||
`And the response should contain`("$.[0]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9")
|
`And the response should contain`("$.[0]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9")
|
||||||
`And the response should contain`("$.[1]citizen.id", "1baf48bb-02bc-4d8f-ac86-33335354f5e7")
|
`And the response should contain`("$.[1]citizen.id", "1baf48bb-02bc-4d8f-ac86-33335354f5e7")
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@ class `Workgroup routes` : BaseTest() {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
} `Then the response should be` OK and {
|
} `Then the response should be` OK and {
|
||||||
`And the response should contain list`("$", 2, 2)
|
`And the response should contain list`("$", 2)
|
||||||
`And the response should contain`("$.[0]citizen.id", "be3b0926-8628-4426-804a-75188a6eb315")
|
`And the response should contain`("$.[0]citizen.id", "be3b0926-8628-4426-804a-75188a6eb315")
|
||||||
`And the response should contain`("$.[1]citizen.id", "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1")
|
`And the response should contain`("$.[1]citizen.id", "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package integration.steps.then
|
package integration.steps.then
|
||||||
|
|
||||||
import assert.assertGreaterThan
|
import assert.assertContain
|
||||||
import assert.assertLessThan
|
|
||||||
import com.jayway.jsonpath.JsonPath
|
import com.jayway.jsonpath.JsonPath
|
||||||
import com.jayway.jsonpath.PathNotFoundException
|
import com.jayway.jsonpath.PathNotFoundException
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
@@ -85,15 +84,13 @@ fun TestApplicationResponse.`And the response should contain pattern`(path: Stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TestApplicationResponse.`And the response should contain list`(path: String, min: Int? = null, max: Int? = null) {
|
fun TestApplicationResponse.`And the response should contain list`(path: String, exactCount: Int) =
|
||||||
|
`And the response should contain list`(path, IntRange(exactCount, exactCount))
|
||||||
|
|
||||||
|
fun TestApplicationResponse.`And the response should contain list`(path: String, range: IntRange) {
|
||||||
JsonPath.read<JSONArray?>(content, path).also {
|
JsonPath.read<JSONArray?>(content, path).also {
|
||||||
assertNotNull(it)
|
assertNotNull(it)
|
||||||
if (min != null) {
|
range assertContain it.size
|
||||||
it.size assertGreaterThan min
|
|
||||||
}
|
|
||||||
if (max != null) {
|
|
||||||
it.size assertLessThan max
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package integration.steps.then
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.node.BooleanNode
|
||||||
|
import com.fasterxml.jackson.databind.node.IntNode
|
||||||
import com.fasterxml.jackson.databind.node.TextNode
|
import com.fasterxml.jackson.databind.node.TextNode
|
||||||
import fr.dcproject.common.utils.getResource
|
import fr.dcproject.common.utils.getResource
|
||||||
|
import fr.dcproject.common.utils.isBool
|
||||||
|
import fr.dcproject.common.utils.isInt
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.Url
|
import io.ktor.http.Url
|
||||||
import io.ktor.request.contentType
|
import io.ktor.request.contentType
|
||||||
@@ -55,15 +59,15 @@ fun TestApplicationResponse.`And the schema response body must be valid`(content
|
|||||||
/* Validate Response */
|
/* Validate Response */
|
||||||
this.apply {
|
this.apply {
|
||||||
val status = call.response.status()
|
val status = call.response.status()
|
||||||
|
val httpMethod = call.request.httpMethod.value.toUpperCase()
|
||||||
val responseContent: JsonNode = if (content != null)
|
val responseContent: JsonNode = if (content != null)
|
||||||
ObjectMapper().readTree(content)
|
ObjectMapper().readTree(content)
|
||||||
else TextNode("")
|
else TextNode("")
|
||||||
|
|
||||||
val response = getResponse(status?.value?.toString() ?: error("HttpStatus not found")) ?: fail("""No Status "${status.value}" found for "$this $uri".""")
|
val response = getResponse(status?.value?.toString() ?: error("HttpStatus not found")) ?: fail("""No Status "${status.value}" found for "$httpMethod $uri".""")
|
||||||
val schema = response.getContentMediaType(contentType.toString())?.schema
|
val schema = response.getContentMediaType(contentType.toString())?.schema
|
||||||
|
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
val httpMethod = call.request.httpMethod.value
|
|
||||||
schema?.validate(api, responseContent)
|
schema?.validate(api, responseContent)
|
||||||
?: fail("""No Status "${status.value}" found with media type "$contentType" for "$httpMethod $uri".""")
|
?: fail("""No Status "${status.value}" found with media type "$contentType" for "$httpMethod $uri".""")
|
||||||
}
|
}
|
||||||
@@ -75,13 +79,18 @@ fun TestApplicationResponse.`And the schema parameters must be valid`() {
|
|||||||
operation { api, uri ->
|
operation { api, uri ->
|
||||||
/* Validate Request URL */
|
/* Validate Request URL */
|
||||||
this.apply {
|
this.apply {
|
||||||
|
val methodName = call.request.httpMethod.value.toUpperCase()
|
||||||
Url(call.request.uri).parameters.forEach { parameter: String, values: List<String> ->
|
Url(call.request.uri).parameters.forEach { parameter: String, values: List<String> ->
|
||||||
val schema = getParametersIn(api.context, "query")
|
val schema = getParametersIn(api.context, "query")
|
||||||
?.firstOrNull { it.name == parameter }?.schema
|
?.firstOrNull { it.name == parameter }?.schema
|
||||||
?: error("""No parameter found ($parameter) for "$this $uri".""")
|
?: error("""No parameter found ($parameter) for "$methodName $uri".""")
|
||||||
|
|
||||||
if (schema.type == "array") {
|
if (schema.type == "array") {
|
||||||
schema.validate(api, ObjectMapper().valueToTree(values))
|
schema.validate(api, ObjectMapper().valueToTree(values))
|
||||||
|
} else if (schema.type == "integer" && values.first().isInt()) {
|
||||||
|
schema.validate(api, IntNode(values.first().toInt()))
|
||||||
|
} else if (schema.type == "boolean" && values.first().isBool()) {
|
||||||
|
schema.validate(api, BooleanNode.valueOf(values.first().toBoolean()))
|
||||||
} else {
|
} else {
|
||||||
schema.validate(api, TextNode(values.first()))
|
schema.validate(api, TextNode(values.first()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import fr.dcproject.component.article.database.ArticleRepository as ArticleRepo
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tags(Tag("security"), Tag("unit"))
|
@Tags(Tag("security"), Tag("unit"), Tag("article"))
|
||||||
internal class `Article Access Control` {
|
internal class `Article Access Control` {
|
||||||
private val tesla = CitizenCreator(
|
private val tesla = CitizenCreator(
|
||||||
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
|
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tags(Tag("security"), Tag("unit"))
|
@Tags(Tag("security"), Tag("unit"), Tag("citizen"))
|
||||||
internal class `Citizen Access Control` {
|
internal class `Citizen Access Control` {
|
||||||
private val tesla = CitizenCart(
|
private val tesla = CitizenCart(
|
||||||
user = User(
|
user = User(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tags(Tag("security"), Tag("unit"))
|
@Tags(Tag("security"), Tag("unit"), Tag("comment"))
|
||||||
internal class `Comment Access Control` {
|
internal class `Comment Access Control` {
|
||||||
private val tesla = Citizen(
|
private val tesla = Citizen(
|
||||||
user = User(
|
user = User(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tags(Tag("security"), Tag("unit"))
|
@Tags(Tag("security"), Tag("unit"), Tag("follow"))
|
||||||
internal class `Follow Access Control` {
|
internal class `Follow Access Control` {
|
||||||
private val tesla = CitizenCreator(
|
private val tesla = CitizenCreator(
|
||||||
user = UserCreator(
|
user = UserCreator(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tags(Tag("security"), Tag("unit"))
|
@Tags(Tag("security"), Tag("unit"), Tag("opinion"))
|
||||||
internal class `Opinion Access Control` {
|
internal class `Opinion Access Control` {
|
||||||
private val tesla = CitizenCreator(
|
private val tesla = CitizenCreator(
|
||||||
user = UserCreator(
|
user = UserCreator(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tags(Tag("security"), Tag("unit"))
|
@Tags(Tag("security"), Tag("unit"), Tag("opinion"))
|
||||||
internal class `OpinionChoice Access Control` {
|
internal class `OpinionChoice Access Control` {
|
||||||
private val tesla = CitizenRef(
|
private val tesla = CitizenRef(
|
||||||
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
|
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tags(Tag("security"), Tag("unit"))
|
@Tags(Tag("security"), Tag("unit"), Tag("vote"))
|
||||||
internal class `Vote Access Control` {
|
internal class `Vote Access Control` {
|
||||||
private val tesla = Citizen(
|
private val tesla = Citizen(
|
||||||
id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"),
|
id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import fr.dcproject.component.workgroup.database.WorkgroupForView as WorkgroupEn
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tags(Tag("security"), Tag("unit"))
|
@Tags(Tag("security"), Tag("unit"), Tag("workgroup"))
|
||||||
internal class `Workgroup Access Control` {
|
internal class `Workgroup Access Control` {
|
||||||
private val tesla = CitizenCreator(
|
private val tesla = CitizenCreator(
|
||||||
user = UserCreator(
|
user = UserCreator(
|
||||||
|
|||||||
Reference in New Issue
Block a user