Test openapi schema response of FindArticlesVersion,GetOneArticle,UpsertArticle

change snack_case to camelCase
This commit is contained in:
2021-03-12 23:32:32 +01:00
parent ed0873837b
commit 9c88adbabd
17 changed files with 432 additions and 156 deletions

View File

@@ -138,11 +138,11 @@ fun Application.module(env: Env = PROD) {
install(ContentNegotiation) { install(ContentNegotiation) {
jackson { jackson {
propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE propertyNamingStrategy = PropertyNamingStrategies.LOWER_CAMEL_CASE
registerModule(JodaModule()) registerModule(JodaModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
configure(SerializationFeature.INDENT_OUTPUT, true) configure(SerializationFeature.INDENT_OUTPUT, true)
setDefaultPrettyPrinter( setDefaultPrettyPrinter(
DefaultPrettyPrinter().apply { DefaultPrettyPrinter().apply {

View File

@@ -29,7 +29,7 @@ data class ArticleForView(
val content: String, val content: String,
val description: String, val description: String,
val tags: List<String> = emptyList(), val tags: List<String> = emptyList(),
override val createdBy: CitizenRef, override val createdBy: CitizenCreator,
override val versionNumber: Int = 0, override val versionNumber: Int = 0,
override val versionId: UUID = UUID.randomUUID(), override val versionId: UUID = UUID.randomUUID(),
val workgroup: WorkgroupCart? = null, val workgroup: WorkgroupCart? = null,
@@ -37,7 +37,7 @@ data class ArticleForView(
override val draft: Boolean = false, override val draft: Boolean = false,
override val deletedAt: DateTime? = null override val deletedAt: DateTime? = null
) : ArticleRef(id), ) : ArticleRef(id),
ArticleAuthI<CitizenRef>, ArticleAuthI<CitizenCreator>,
ArticleWithTitleI, ArticleWithTitleI,
Versionable, Versionable,
CreatedAt by CreatedAt.Imp(), CreatedAt by CreatedAt.Imp(),
@@ -79,9 +79,10 @@ class ArticleForListing(
id: UUID? = null, id: UUID? = null,
override val title: String, override val title: String,
override val createdBy: CitizenCreator, override val createdBy: CitizenCreator,
override val workgroup: WorkgroupCart?, override val workgroup: WorkgroupCart? = null,
override val deletedAt: DateTime?, override val deletedAt: DateTime? = null,
override val draft: Boolean override val draft: Boolean = false,
val lastVersion: Boolean = false
) : ArticleForListingI, ) : ArticleForListingI,
ArticleRef(id), ArticleRef(id),
ArticleAuthI<CitizenCartI>, ArticleAuthI<CitizenCartI>,

View File

@@ -13,13 +13,13 @@ class ArticleRepository(override var requester: Requester) : RepositoryI {
return function.selectOne("id" to id) return function.selectOne("id" to id)
} }
fun findVersionsById(page: Int = 1, limit: Int = 50, id: UUID): Paginated<ArticleForView> { fun findVersionsById(page: Int = 1, limit: Int = 50, id: UUID): Paginated<ArticleForListing> {
return requester return requester
.getFunction("find_articles_versions_by_id") .getFunction("find_articles_versions_by_id")
.select(page, limit, "id" to id) .select(page, limit, "id" to id)
} }
fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForView> { fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForListing> {
return requester return requester
.getFunction("find_articles_versions_by_version_id") .getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to versionId) .select(page, limit, "version_id" to versionId)

View File

@@ -1,7 +1,9 @@
package fr.dcproject.component.article.routes package fr.dcproject.component.article.routes
import fr.dcproject.common.dto.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.article.ArticleAccessControl import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.database.ArticleForListing
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
@@ -37,7 +39,34 @@ object FindArticleVersions {
get<ArticleVersionsRequest> { get<ArticleVersionsRequest> {
repo.findVersions(it) repo.findVersions(it)
.apply { ac.assert { canView(result, citizenOrNull) } } .apply { ac.assert { canView(result, citizenOrNull) } }
.let { call.respond(it) } .run {
call.respond(
toOutput { a: ArticleForListing ->
object {
val id = a.id
val title = a.title
val createdBy = object {
val id = a.createdBy.id
val name = a.createdBy.name.let { n ->
object {
val firstName = n.firstName
val lastName = n.lastName
}
}
val email = a.createdBy.email
}
val workgroup = a.workgroup?.let { w ->
object {
val id = w.id
val name = w.name
}
}
val draft = a.draft
val lastVersion = a.lastVersion
}
}
)
}
} }
} }
} }

View File

@@ -1,7 +1,5 @@
package fr.dcproject.component.article.routes package fr.dcproject.component.article.routes
import fr.dcproject.common.dto.CreatedAt
import fr.dcproject.common.dto.Versionable
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.article.ArticleAccessControl import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.ArticleViewManager import fr.dcproject.component.article.ArticleViewManager
@@ -9,10 +7,6 @@ import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.opinion.dto.Opinionable
import fr.dcproject.component.views.dto.Viewable
import fr.dcproject.component.views.entity.ViewAggregation
import fr.dcproject.component.vote.dto.Votable
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -30,39 +24,56 @@ object GetOneArticle {
val article = ArticleRef(article) val article = ArticleRef(article)
} }
class Output(
article: ArticleForView,
views: ViewAggregation = ViewAggregation()
) : CreatedAt by CreatedAt.Imp(article),
Opinionable by Opinionable.Imp(article),
Votable by Votable.Imp(article),
Versionable by Versionable.Imp(article),
Viewable by Viewable.Imp(views) {
val id = article.id
val title = article.title
val anonymous = article.anonymous
val content = article.content
val description = article.description
val tags = article.tags
val draft = article.draft
val lastVersion = article.lastVersion
val createdBy = article.createdBy
val workgroup = article.workgroup?.let { Workgroup(article.workgroup.id, article.workgroup.name) }
class Workgroup(val id: UUID, val name: String)
}
fun Route.getOneArticle(viewManager: ArticleViewManager<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) { fun Route.getOneArticle(viewManager: ArticleViewManager<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
get<ArticleRequest> { get<ArticleRequest> {
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found") val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
ac.assert { canView(article, citizenOrNull) } ac.assert { canView(article, citizenOrNull) }
Output( call.respond(
article, article.let { a ->
viewManager.getViewsCount(article) object {
).also { out -> val id = a.id
call.respond(out) val versionId = a.versionId
val versionNumber = a.versionNumber
val title = a.title
val anonymous = a.anonymous
val content = a.content
val description = a.description
val tags = a.tags
val draft = a.draft
val lastVersion = a.lastVersion
val createdAt = a.createdAt
val createdBy: Any = object {
val id: UUID = a.createdBy.id
val name: Any = object {
val firstName: String = a.createdBy.name.firstName
val lastName: String = a.createdBy.name.lastName
} }
val email: String = a.createdBy.email
}
val workgroup: Any? = a.workgroup?.let { w ->
object {
val id: UUID = w.id
val name: String = w.name
}
}
val votes: Any = object {
val up: Int = a.votes.up
val neutral: Int = a.votes.neutral
val down: Int = a.votes.down
val total: Int = a.votes.total
val score: Int = a.votes.score
}
val views: Any = viewManager.getViewsCount(article).let { v ->
object {
val total = v.total
val unique = v.unique
}
}
val opinions: Map<String, Int> = a.opinions
}
}
)
launch { launch {
viewManager.addView(call.request.local.remoteHost, article, citizenOrNull) viewManager.addView(call.request.local.remoteHost, article, citizenOrNull)

View File

@@ -4,7 +4,6 @@ import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.ArticleAccessControl import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.database.ArticleForUpdate import fr.dcproject.component.article.database.ArticleForUpdate
import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.Input import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.Input
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
@@ -57,9 +56,16 @@ object UpsertArticle {
post<UpsertArticleRequest> { post<UpsertArticleRequest> {
val article = call.convertRequestToEntity() val article = call.convertRequestToEntity()
ac.assert { canUpsert(article, citizenOrNull) } ac.assert { canUpsert(article, citizenOrNull) }
val newArticle: ArticleForView = repo.upsert(article) ?: error("Article not updated") repo.upsert(article)?.let { a ->
call.respond(newArticle) call.respond(
publisher.publish(ArticleUpdateNotification(newArticle)) object {
val id: UUID = a.id
val versionId = a.versionId
val versionNumber = a.versionNumber
}
)
publisher.publish(ArticleUpdateNotification(a))
} ?: error("Article not updated")
} }
} }
} }

View File

@@ -1,6 +1,6 @@
openapi: 3.0.2 openapi: 3.0.2
info: info:
version: '0.1' version: ''
title: 'DC Project' title: 'DC Project'
description: 'A free comunity program for create constitution' description: 'A free comunity program for create constitution'
@@ -46,7 +46,7 @@ paths:
format: uuid format: uuid
title: title:
type: string type: string
created_by: createdBy:
type: object type: object
additionalProperties: false additionalProperties: false
properties: properties:
@@ -56,9 +56,9 @@ paths:
name: name:
type: object type: object
properties: properties:
first_name: firstName:
type: string type: string
last_name: lastName:
type: string type: string
email: email:
type: string type: string
@@ -74,6 +74,71 @@ paths:
type: string type: string
draft: draft:
type: boolean type: boolean
post:
security:
- JWTAuth: []
summary: Create new Article
tags:
- article
operationId: insertArticle
requestBody:
required: true
content:
application/json:
schema:
required:
- title
- content
- description
- tags
- anonymous
- draft
properties:
title:
type: string
example:
Limit power of press
content:
type: string
example:
Lorem upsum...
description:
type: string
example:
I think is the bether choice
tags:
type: array
items:
type: string
default: [ ]
example: [ power, press ]
anonymous:
type: boolean
default: true
draft:
type: boolean
default: false
workgroup:
allOf:
- $ref: '#/components/schemas/UuidEntity'
- default: null
responses:
200:
description: Article created
content:
application/json:
schema:
properties:
id:
type: string
format: uuid
versionId:
type: string
format: uuid
versionNumber:
type: integer
401:
$ref: '#/components/responses/401'
/articles/{article}: /articles/{article}:
parameters: parameters:
- $ref: '#/components/parameters/article' - $ref: '#/components/parameters/article'
@@ -90,6 +155,29 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ArticleResponse'
/articles/{article}/versions:
parameters:
- $ref: '#/components/parameters/article'
get:
summary: Get all versions of articles
tags:
- article
operationId: getArticleVersions
responses:
200:
description: The versions of Article
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Paginated'
- type: object
properties:
result:
type: array
items:
additionalProperties: false additionalProperties: false
properties: properties:
id: id:
@@ -97,21 +185,11 @@ paths:
format: uuid format: uuid
title: title:
type: string type: string
anonymous:
type: boolean
content:
type: string
description:
type: string
tags:
type: array
items:
type: string
draft: draft:
type: boolean type: boolean
last_version: lastVersion:
type: boolean type: boolean
created_by: createdBy:
type: object type: object
additionalProperties: false additionalProperties: false
properties: properties:
@@ -121,9 +199,9 @@ paths:
name: name:
type: object type: object
properties: properties:
first_name: firstName:
type: string type: string
last_name: lastName:
type: string type: string
email: email:
type: string type: string
@@ -165,7 +243,7 @@ components:
name: sort name: sort
in: query in: query
description: The sort field name description: The sort field name
example: first_name example: firstName
required: false required: false
schema: schema:
type: string type: string
@@ -220,6 +298,14 @@ components:
type: string type: string
format: uuid format: uuid
responses:
401:
description: Unautorized
content:
application/json:
schema:
description: noting
schemas: schemas:
UUID: UUID:
type: string type: string
@@ -260,6 +346,120 @@ components:
type: integer type: integer
minimum: 0 minimum: 0
example: 1 example: 1
ArticleResponse:
additionalProperties: false
required:
- id
- versionId
- versionNumber
- title
- anonymous
- content
- description
- tags
- draft
- lastVersion
- createdBy
- views
- opinions
- votes
properties:
id:
type: string
format: uuid
versionId:
type: string
format: uuid
versionNumber:
type: integer
minimum: 0
title:
type: string
anonymous:
type: boolean
content:
type: string
description:
type: string
tags:
type: array
items:
type: string
draft:
type: boolean
lastVersion:
type: boolean
createdAt:
type: string
format: 'date-time'
createdBy:
type: object
additionalProperties: false
required:
- id
- name
- email
properties:
id:
type: string
format: uuid
name:
type: object
required:
- firstName
- lastName
properties:
firstName:
type: string
lastName:
type: string
email:
type: string
workgroup:
type: object
nullable: true
additionalProperties: false
properties:
id:
type: string
format: uuid
name:
type: string
views:
additionalProperties: false
required:
- total
- unique
properties:
total:
type: integer
unique:
type: integer
opinions:
additionalProperties:
type: integer
example:
'good writed': 5
'to short': 19
votes:
additionalProperties: false
required:
- up
- neutral
- down
- total
- score
properties:
up:
type: integer
neutral:
type: integer
down:
type: integer
total:
type: integer
score:
type: integer
securitySchemes: securitySchemes:
JWTAuth: JWTAuth:

View File

@@ -13,9 +13,16 @@ begin
into resource, total into resource, total
from ( from (
select select
a.*, a.id,
a.created_at,
find_citizen_by_id_with_user(a.created_by_id) as created_by, find_citizen_by_id_with_user(a.created_by_id) as created_by,
find_workgroup_by_id(a.workgroup_id) as workgroup, find_workgroup_by_id(a.workgroup_id) as workgroup,
a.version_id,
a.version_number,
a.title,
a.deleted_at,
a.draft,
a.last_version,
count_vote(a.id) as votes count_vote(a.id) as votes
from article as a from article as a
where a.version_id = _version_id where a.version_id = _version_id

View File

@@ -8,7 +8,6 @@ import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowForView import fr.dcproject.component.follow.database.FollowForView
import fr.dcproject.component.notification.ArticleUpdateNotification import fr.dcproject.component.notification.ArticleUpdateNotification
@@ -108,7 +107,11 @@ class NotificationConsumerTest {
title = "MyTitle", title = "MyTitle",
content = "myContent", content = "myContent",
description = "myDescription", description = "myDescription",
createdBy = CitizenRef() createdBy = CitizenCreator(
name = CitizenI.Name(firstName = "", lastName = ""),
email = "",
user = UserCreator(username = ""),
)
) )
) )
).await() ).await()

View File

@@ -3,7 +3,9 @@ package functional
import com.rabbitmq.client.ConnectionFactory import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.application.Configuration import fr.dcproject.application.Configuration
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.notification.ArticleUpdateNotification import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.Notification import fr.dcproject.component.notification.Notification
import fr.dcproject.component.notification.NotificationsPush import fr.dcproject.component.notification.NotificationsPush
@@ -53,7 +55,11 @@ internal class NotificationsPushTest {
every { redisClient.connect().async() } returns asyncCommand every { redisClient.connect().async() } returns asyncCommand
/* Citizen of notification */ /* Citizen of notification */
val citizen = CitizenRef() val citizen = CitizenCreator(
name = CitizenI.Name(firstName = "", lastName = ""),
email = "",
user = UserCreator(username = ""),
)
/* Article is the target of the notification */ /* Article is the target of the notification */
val article = ArticleForView( val article = ArticleForView(
content = "content..", content = "content..",

View File

@@ -4,6 +4,9 @@ import fr.dcproject.application.Env.TEST
import fr.dcproject.application.module import fr.dcproject.application.module
import fr.dcproject.component.article.ArticleViewManager import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.server.testing.withTestApplication import io.ktor.server.testing.withTestApplication
@@ -27,9 +30,13 @@ class ViewTest {
@Test @Test
fun `test View Article`() { fun `test View Article`() {
val article = ArticleForView( val article = ArticleForView(
id = UUID.randomUUID(),
versionId = UUID.randomUUID(), versionId = UUID.randomUUID(),
createdBy = CitizenRef(), createdBy = CitizenCreator(
id = UUID.randomUUID(),
name = CitizenI.Name(firstName = "", lastName = ""),
email = "",
user = UserCreator(username = ""),
),
content = "", content = "",
description = "", description = "",
title = "" title = ""

View File

@@ -32,9 +32,9 @@ class `Article routes` : BaseTest() {
`Given I have articles`(3) `Given I have articles`(3)
`When I send a GET request`("/articles") `Then the response should be` OK and { `When I send a GET request`("/articles") `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain pattern`("$.result[0].created_by.name.first_name", "firstName.+") `And the response should contain pattern`("$.result[0].createdBy.name.firstName", "firstName.+")
`And the response should contain pattern`("$.result[1].created_by.name.first_name", "firstName.+") `And the response should contain pattern`("$.result[1].createdBy.name.firstName", "firstName.+")
`And the response should contain pattern`("$.result[2].created_by.name.first_name", "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, 3)
`And schema must be valid`() `And schema must be valid`()
@@ -77,6 +77,7 @@ class `Article routes` : BaseTest() {
`And the response should not be null`() `And the response should not be null`()
`And have property`("$.total") `whish contains` 1 `And have property`("$.total") `whish contains` 1
`And have property`("$.result[0].id") `whish contains` "13e6091c-8fed-4600-b079-a97a6b7a9800" `And have property`("$.result[0].id") `whish contains` "13e6091c-8fed-4600-b079-a97a6b7a9800"
`And schema must be valid`()
} }
} }
} }
@@ -89,7 +90,7 @@ class `Article routes` : BaseTest() {
`authenticated as`("John", "Doe") `authenticated as`("John", "Doe")
""" """
{ {
"version_id": "09c418b6-63ba-448b-b38b-502b41cd500e", "versionId": "09c418b6-63ba-448b-b38b-502b41cd500e",
"title": "title2", "title": "title2",
"anonymous": false, "anonymous": false,
"content": "content2", "content": "content2",
@@ -101,8 +102,8 @@ class `Article routes` : BaseTest() {
""" """
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And have property`("$.version_id") `whish contains` "09c418b6-63ba-448b-b38b-502b41cd500e" `And have property`("$.versionId") `whish contains` "09c418b6-63ba-448b-b38b-502b41cd500e"
`And have property`("$.title") `whish contains` "title2" `And schema must be valid`()
} }
} }
} }

View File

@@ -3,10 +3,13 @@ package unit.security
import fr.dcproject.common.security.AccessDecision.DENIED import fr.dcproject.common.security.AccessDecision.DENIED
import fr.dcproject.common.security.AccessDecision.GRANTED import fr.dcproject.common.security.AccessDecision.GRANTED
import fr.dcproject.component.article.ArticleAccessControl import fr.dcproject.component.article.ArticleAccessControl
import fr.dcproject.component.article.database.ArticleForListing
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.auth.database.User import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserI import fr.dcproject.component.auth.database.UserI
import fr.dcproject.component.citizen.database.CitizenCart import fr.dcproject.component.citizen.database.CitizenCart
import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import io.mockk.every import io.mockk.every
@@ -26,13 +29,13 @@ import fr.dcproject.component.article.database.ArticleRepository as ArticleRepo
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"))
internal class `Article Access Control` { internal class `Article Access Control` {
private val tesla = CitizenCart( private val tesla = CitizenCreator(
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"), id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
user = User( user = UserCreator(
username = "nicolas-tesla", username = "nicolas-tesla",
roles = listOf(UserI.Roles.ROLE_USER)
), ),
name = CitizenI.Name("Nicolas", "Tesla") name = CitizenI.Name("Nicolas", "Tesla"),
email = "nikola-tesla@volt.com"
) )
private val einstein = CitizenCart( private val einstein = CitizenCart(
user = User( user = User(
@@ -42,16 +45,16 @@ internal class `Article Access Control` {
name = CitizenI.Name("Albert", "Einstein") name = CitizenI.Name("Albert", "Einstein")
) )
private fun getRepo(article: ArticleForView): ArticleRepo { private fun getRepo(article: ArticleForListing): ArticleRepo {
return mockk { return mockk {
every { findVersionsByVersionId(1, 1, any()) } returns Paginated(listOf(article), 0, 1, 1) every { find(1, 1, any()) } returns Paginated(listOf(article), 0, 1, 1)
} }
} }
@Test @Test
fun `creator can be view the article`() { fun `creator can be view the article`() {
val article = getArticle(tesla).copy(draft = true) val article = getArticle(tesla).copy(draft = true)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canView(article, tesla) .canView(article, tesla)
.decision `should be` GRANTED .decision `should be` GRANTED
} }
@@ -59,7 +62,7 @@ internal class `Article Access Control` {
@Test @Test
fun `other user can be view the article`() { fun `other user can be view the article`() {
val article = getArticle(tesla) val article = getArticle(tesla)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canView(article, einstein) .canView(article, einstein)
.decision `should be` GRANTED .decision `should be` GRANTED
} }
@@ -69,7 +72,7 @@ internal class `Article Access Control` {
val article = getArticle(tesla) val article = getArticle(tesla)
val article2 = getArticle(tesla) val article2 = getArticle(tesla)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canView(listOf(article, article2), einstein) .canView(listOf(article, article2), einstein)
.decision `should be` GRANTED .decision `should be` GRANTED
} }
@@ -77,7 +80,7 @@ internal class `Article Access Control` {
@Test @Test
fun `the no creator can not be view the article on draft`() { fun `the no creator can not be view the article on draft`() {
val article = getArticle(tesla).copy(draft = true) val article = getArticle(tesla).copy(draft = true)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canView(article, einstein) .canView(article, einstein)
.decision `should be` DENIED .decision `should be` DENIED
} }
@@ -87,7 +90,7 @@ internal class `Article Access Control` {
val article = getArticle(tesla) val article = getArticle(tesla)
val article2 = getArticle(tesla).copy(draft = true) val article2 = getArticle(tesla).copy(draft = true)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canView(listOf(article, article2), einstein) .canView(listOf(article, article2), einstein)
.decision `should be` DENIED .decision `should be` DENIED
} }
@@ -95,7 +98,7 @@ internal class `Article Access Control` {
@Test @Test
fun `can not view deleted article`() { fun `can not view deleted article`() {
val article = getArticle(tesla).copy(deletedAt = DateTime.now()) val article = getArticle(tesla).copy(deletedAt = DateTime.now())
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canView(article, tesla) .canView(article, tesla)
.decision `should be` DENIED .decision `should be` DENIED
} }
@@ -103,7 +106,7 @@ internal class `Article Access Control` {
@Test @Test
fun `can delete article if owner`() { fun `can delete article if owner`() {
val article = getArticle(tesla) val article = getArticle(tesla)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canDelete(article, tesla) .canDelete(article, tesla)
.decision `should be` GRANTED .decision `should be` GRANTED
} }
@@ -111,7 +114,7 @@ internal class `Article Access Control` {
@Test @Test
fun `can not delete article if not owner`() { fun `can not delete article if not owner`() {
val article = getArticle(tesla).copy(deletedAt = DateTime.now()) val article = getArticle(tesla).copy(deletedAt = DateTime.now())
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canDelete(article, einstein) .canDelete(article, einstein)
.code `should be` "article.delete.notYours" .code `should be` "article.delete.notYours"
} }
@@ -119,7 +122,7 @@ internal class `Article Access Control` {
@Test @Test
fun `can create article if logged`() { fun `can create article if logged`() {
val article = getArticle(tesla) val article = getArticle(tesla)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canUpsert(article, tesla) .canUpsert(article, tesla)
.decision `should be` GRANTED .decision `should be` GRANTED
} }
@@ -127,7 +130,7 @@ internal class `Article Access Control` {
@Test @Test
fun `can not create article if not logged`() { fun `can not create article if not logged`() {
val article = getArticle(tesla) val article = getArticle(tesla)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canUpsert(article, null) .canUpsert(article, null)
.code `should be` "article.create.notConnected" .code `should be` "article.create.notConnected"
} }
@@ -135,7 +138,7 @@ internal class `Article Access Control` {
@Test @Test
fun `can update article if yours`() { fun `can update article if yours`() {
val article = getArticle(tesla) val article = getArticle(tesla)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canUpsert(article, tesla) .canUpsert(article, tesla)
.decision `should be` GRANTED .decision `should be` GRANTED
} }
@@ -143,12 +146,12 @@ internal class `Article Access Control` {
@Test @Test
fun `can not update article if not yours`() { fun `can not update article if not yours`() {
val article = getArticle(tesla) val article = getArticle(tesla)
ArticleAccessControl(getRepo(article)) ArticleAccessControl(getRepo(getArticleForListing(tesla)))
.canUpsert(article, einstein) .canUpsert(article, einstein)
.code `should be` "article.update.notYours" .code `should be` "article.update.notYours"
} }
private fun getArticle(createdBy: CitizenCart = tesla) = ArticleForView( private fun getArticle(createdBy: CitizenCreator = tesla) = ArticleForView(
id = UUID.randomUUID(), id = UUID.randomUUID(),
title = "Hello world", title = "Hello world",
content = "Super", content = "Super",
@@ -158,4 +161,10 @@ internal class `Article Access Control` {
versionId = UUID.randomUUID(), versionId = UUID.randomUUID(),
versionNumber = 1 versionNumber = 1
) )
private fun getArticleForListing(createdBy: CitizenCreator = tesla) = ArticleForListing(
id = UUID.randomUUID(),
title = "Hello world",
createdBy = createdBy,
)
} }

View File

@@ -5,9 +5,10 @@ import fr.dcproject.common.security.AccessDecision.GRANTED
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.database.User import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserI import fr.dcproject.component.auth.database.UserI
import fr.dcproject.component.citizen.database.Citizen import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenCart import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate import fr.dcproject.component.comment.generic.database.CommentForUpdate
@@ -46,13 +47,13 @@ internal class `Comment Access Control` {
name = CitizenI.Name("Albert", "Einstein") name = CitizenI.Name("Albert", "Einstein")
) )
private val einstein2 = CitizenCart( private val einstein2 = CitizenCreator(
id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"), id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"),
user = User( user = UserCreator(
username = "albert-einstein", username = "albert-einstein",
roles = listOf(UserI.Roles.ROLE_USER)
), ),
name = CitizenI.Name("Albert", "Einstein") name = CitizenI.Name("Albert", "Einstein"),
email = "albert-einstein@email.com"
) )
private val article1 = ArticleForView( private val article1 = ArticleForView(

View File

@@ -7,7 +7,6 @@ import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserI import fr.dcproject.component.auth.database.UserI
import fr.dcproject.component.citizen.database.Citizen import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenCart
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
@@ -55,13 +54,13 @@ internal class `Follow Access Control` {
followAnonymous = true followAnonymous = true
) )
private val einstein2 = CitizenCart( private val einstein2 = CitizenCreator(
id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"), id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"),
user = User( user = UserCreator(
username = "albert-einstein", username = "albert-einstein",
roles = listOf(UserI.Roles.ROLE_USER)
), ),
name = CitizenI.Name("Albert", "Einstein") name = CitizenI.Name("Albert", "Einstein"),
email = "albert-einstein@email.com"
) )
private val einstein3 = Citizen( private val einstein3 = Citizen(

View File

@@ -3,10 +3,7 @@ package unit.security
import fr.dcproject.common.security.AccessDecision.DENIED import fr.dcproject.common.security.AccessDecision.DENIED
import fr.dcproject.common.security.AccessDecision.GRANTED import fr.dcproject.common.security.AccessDecision.GRANTED
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserI
import fr.dcproject.component.citizen.database.CitizenCart
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
@@ -39,13 +36,13 @@ internal class `Opinion Access Control` {
id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"), id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"),
) )
private val einstein2 = CitizenCart( private val einstein2 = CitizenCreator(
id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"), id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"),
user = User( user = UserCreator(
username = "albert-einstein", username = "albert-einstein",
roles = listOf(UserI.Roles.ROLE_USER)
), ),
name = CitizenI.Name("Albert", "Einstein") name = CitizenI.Name("Albert", "Einstein"),
email = "albert-einstein@email.com"
) )
private val article1 = ArticleForView( private val article1 = ArticleForView(

View File

@@ -7,7 +7,6 @@ import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserI import fr.dcproject.component.auth.database.UserI
import fr.dcproject.component.citizen.database.Citizen import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenCart
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
@@ -60,13 +59,13 @@ internal class `Vote Access Control` {
followAnonymous = true followAnonymous = true
) )
private val einstein2 = CitizenCart( private val einstein2 = CitizenCreator(
id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"), id = UUID.fromString("319f1226-8f47-4df3-babd-2c7671ad0fbc"),
user = User( user = UserCreator(
username = "albert-einstein", username = "albert-einstein",
roles = listOf(UserI.Roles.ROLE_USER)
), ),
name = CitizenI.Name("Albert", "Einstein") name = CitizenI.Name("Albert", "Einstein"),
email = "albert-einstein@email.com"
) )
private val article1 = ArticleForView( private val article1 = ArticleForView(