Continue to implement opinion

improve target reference
Improve Tests for Opinion
fix SQL:upsert_opinion
This commit is contained in:
2020-02-14 01:26:47 +01:00
parent 60bd24e653
commit 471013984c
42 changed files with 683 additions and 137 deletions

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
dcproject

2
.idea/misc.xml generated
View File

@@ -7,7 +7,7 @@
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_13" default="true" project-jdk-name="openjdk-13.0.2" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="adopt-openjdk-11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,14 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests + Lint" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="ALTERNATIVE_JRE_PATH" value="11" />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="directory" />
<option name="TEST_OBJECT" value="package" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="TEST_SEARCH_SCOPE">

View File

@@ -0,0 +1,26 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests + Lint (offline)" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="tags" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.options=&quot;--tags ~@online&quot;" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Lint" run_configuration_type="GradleRunConfiguration" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Test All SQL" run_configuration_type="RunSql" />
</method>
</configuration>
</component>

View File

@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests (offline)" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.options=&quot;--tags ~@online&quot;" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -34,7 +34,7 @@
<method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All Tests + Lint" run_configuration_type="JUnit" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Sonarqube" run_configuration_type="GradleRunConfiguration" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Fixtures on Dev" run_configuration_type="RunSql" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Reset database schema and import Fixtures" run_configuration_type="RunSql" />
</method>
</configuration>
</component>

View File

@@ -32,7 +32,7 @@
</extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Fixtures on Dev" run_configuration_type="RunSql" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Reset database schema and import Fixtures" run_configuration_type="RunSql" />
</method>
</configuration>
</component>

View File

@@ -0,0 +1,28 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Reset database schema and import Fixtures" type="RunSql" factoryName="Run SQL">
<option name="files">
<list>
<option value="$PROJECT_DIR$/src/main/resources/sql/migrations/0000-init_schema.down.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/migrations/0000-init_schema.up.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/01-user.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/02-citizen.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/03-workgroup.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/04-article.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/05-constitution.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/06-follow.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/07-comment.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/08-vote.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/fixtures/09-opinion.sql" />
</list>
</option>
<option name="targets">
<list>
<Target>
<option name="dsId" value="28368159-3c2d-4612-8719-e55ce11b962a" />
<option name="namespace" value="database/&quot;dc-project&quot;/schema/&quot;public&quot;" />
</Target>
</list>
</option>
<method v="2" />
</configuration>
</component>

View File

@@ -38,7 +38,7 @@
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/helpers/find_reference_by_id.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/helpers/random_between.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/count_opinion.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/opinion.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/upsert_opinion.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/user/change_user_password.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/user/check_user.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/user/find_user_by_id.sql" />
@@ -52,9 +52,11 @@
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/find_citizen_opinions.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/find_citizen_opinions_by_target_id.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/find_citizen_opinions_by_target_ids.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/find_opinion_by_opinion.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/find_opinion_choice_by_id.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/find_opinion_choices.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/opinion.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/find_opinion_by_id.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/opinion/upsert_opinion.sql" />
<option value="$PROJECT_DIR$/src/test/sql/user.sql" />
<option value="$PROJECT_DIR$/src/test/sql/citizen.sql" />
<option value="$PROJECT_DIR$/src/test/sql/article.sql" />

View File

@@ -5,6 +5,6 @@ logback_version=1.2.1
postgresjson_version=0.1
koinVersion=2.0.1
jackson_version=2.9.9
cucumber_version=4.7.1
cucumber_version=5.1.3
systemProp.sonar.host.url=http://localhost:9000
systemProp.sonar.login=1196e8015c20035f1aa91e881b95ce9d6e879c8a

View File

@@ -197,6 +197,7 @@ fun Application.module(env: Env = PROD) {
commentConstitution(get())
voteArticle(get(), get(), get())
voteConstitution(get())
opinionArticle(get())
opinionChoice(get())
definition()
}
@@ -215,6 +216,9 @@ fun Application.module(env: Env = PROD) {
exception<NotFoundException> { e ->
call.respond(HttpStatusCode.BadRequest, e.message!!)
}
exception<ForbiddenException> {
call.respond(HttpStatusCode.Forbidden)
}
}
install(CORS) {

View File

@@ -32,8 +32,6 @@ open class Comment<T : TargetI>(
target = parent.target,
content = content
)
override val reference get() = TargetI.getReference(this)
}
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentS(id)

View File

@@ -6,7 +6,7 @@ import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.immutable.UuidEntityI
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf
import kotlin.reflect.full.isSubclassOf
interface ExtraI<T : TargetI> :
UuidEntityI,
@@ -26,16 +26,18 @@ interface TargetI : UuidEntityI {
enum class TargetName(val targetReference: String) {
Article("article"),
Constitution("constitution"),
Comment("comment")
Comment("comment"),
Opinion("opinion")
}
companion object {
fun <T : TargetI> getReference(t: KClass<T>): String {
return when {
t.isSuperclassOf(Article::class) -> TargetName.Article.targetReference
t.isSuperclassOf(Constitution::class) -> TargetName.Constitution.targetReference
t.isSuperclassOf(Comment::class) -> TargetName.Comment.targetReference
else -> throw error("target not implemented")
t.isSubclassOf(ArticleRef::class) -> TargetName.Article.targetReference
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(Opinion::class) -> TargetName.Opinion.targetReference
else -> throw error("target not implemented: ${t.qualifiedName}")
}
}

View File

@@ -1,6 +1,9 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.immutable.EntityCreatedAt
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
import fr.postgresjson.entity.immutable.EntityCreatedBy
import fr.postgresjson.entity.immutable.EntityCreatedByImp
import java.util.*
open class Opinion<T : TargetI>(
@@ -9,11 +12,16 @@ open class Opinion<T : TargetI>(
override val target: T,
val choice: OpinionChoice
) : ExtraI<T>,
UuidEntity(id),
TargetRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy) {
fun getName(): String = choice.name
}
typealias OpinionArticle = Opinion<Article>
class OpinionArticle(
id: UUID = UUID.randomUUID(),
createdBy: CitizenBasic,
target: ArticleRef,
choice: OpinionChoice
) : Opinion<ArticleRef>(id, createdBy, target, choice)

View File

@@ -10,7 +10,7 @@ import java.util.*
class OpinionChoice(
id: UUID,
val name: String,
val target: List<String>
val target: List<String>?
) : OpinionChoiceRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp()

View File

@@ -3,13 +3,13 @@ package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
class OpinionAggregation(
override val entries: Set<Map.Entry<String, Int>> = emptySet()
) : AbstractMap<String, Int>(), EntityI
private val underlying: MutableMap<String, Any> = mutableMapOf()
) : MutableMap<String, Any> by underlying, EntityI
interface Opinionable {
val opinions: MutableMap<String, Int>
var opinions: MutableMap<String, Int>
}
class OpinionableImp : Opinionable {
override val opinions: MutableMap<String, Int> = mutableMapOf()
override var opinions: MutableMap<String, Int> = mutableMapOf()
}

View File

@@ -1,27 +0,0 @@
package fr.dcproject.entity.request
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.OpinionArticle
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.TargetRef
import fr.dcproject.repository.Article
import fr.dcproject.repository.OpinionChoice
import fr.dcproject.utils.toUUID
import org.koin.core.KoinComponent
import org.koin.core.get
class ArticleOpinionRequest(
target: String,
opinionChoice: String
) : RequestBuilderWithCreator<Citizen, OpinionArticle>, KoinComponent {
val target = TargetRef(target.toUUID())
val opinionChoice = OpinionChoiceRef(opinionChoice.toUUID())
override fun create(citizen: Citizen): OpinionArticle {
return OpinionArticle(
choice = get<OpinionChoice>().findOpinionChoiceById(opinionChoice.id)!!,
target = get<Article>().findById(target.id)!!,
createdBy = citizen
)
}
}

View File

@@ -1,14 +1,11 @@
package fr.dcproject.entity.request
import fr.dcproject.entity.CitizenRef
import fr.postgresjson.entity.EntityI
import io.ktor.application.ApplicationCall
interface Request
interface RequestBuilder<E: EntityI> : Request {
fun create(): E
interface RequestBuilder<E> {
suspend fun getContent(call: ApplicationCall): E
}
interface RequestBuilderWithCreator<C: CitizenRef, E: EntityI> : Request {
fun create(citizen: C): E
}
suspend fun <E> ApplicationCall.getContent(builder: RequestBuilder<E>) = builder.getContent(this)

View File

@@ -11,6 +11,7 @@ import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Opinion as OpinionEntity
import fr.dcproject.entity.OpinionArticle as OpinionArticleEntity
import fr.dcproject.entity.OpinionChoice as OpinionChoiceEntity
open class OpinionChoice(override val requester: Requester) : RepositoryI {
@@ -25,6 +26,14 @@ open class OpinionChoice(override val requester: Requester) : RepositoryI {
"targets" to targets
)
/**
* find opinion choices by name
*/
fun findOpinionsChoiceByName(name: String): OpinionChoiceEntity? =
findOpinionsChoices().first {
it.name == name
}
/**
* find one opinion choices by id
*/
@@ -104,9 +113,18 @@ open class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requeste
.select(page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"citizen" to citizen.id
"citizen_id" to citizen.id
)
}
}
class OpinionArticle(requester: Requester) : Opinion<Article>(requester)
class OpinionArticle(requester: Requester) : Opinion<Article>(requester) {
/**
* Create an Opinion on Article
*/
fun opinion(opinion: OpinionArticleEntity): OpinionArticleEntity {
return requester
.getFunction("upsert_opinion")
.selectOne(opinion) ?: error("query 'upsert_opinion' return null")
}
}

View File

@@ -1,11 +1,18 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.request.ArticleOpinionRequest
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.OpinionArticle
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.request.RequestBuilder
import fr.dcproject.entity.request.getContent
import fr.dcproject.repository.OpinionChoice
import fr.dcproject.security.voter.OpinionVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.dcproject.utils.toUUID
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.features.BadRequestException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
@@ -17,6 +24,7 @@ import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
import org.koin.core.KoinComponent
import org.koin.core.get
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
@@ -24,7 +32,7 @@ import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI
object OpinionArticlePaths {
/**
* Get paginated opinion of citizen for one article
* Get paginated opinions of citizen for all articles
*/
@Location("/citizens/{citizen}/opinions/articles")
class CitizenOpinionArticleRequest(
@@ -36,16 +44,37 @@ object OpinionArticlePaths {
/**
* Put an opinion on one article
*/
@Location("/articles/{article}/opinons")
class ArticleOpinion(val article: ArticleEntity)
@Location("/articles/{article}/opinions")
@KtorExperimentalAPI
class ArticleOpinion(val article: ArticleEntity) : RequestBuilder<OpinionArticle> {
private class Content(
opinionChoice: String
) : KoinComponent {
val opinionChoice = OpinionChoiceRef(opinionChoice.toUUID())
fun create(citizen: Citizen, article: ArticleEntity): OpinionArticle {
return OpinionArticle(
choice = get<OpinionChoice>().findOpinionChoiceById(opinionChoice.id) ?: throw BadRequestException("OpinionChoice not exist: id(${opinionChoice.id})"),
target = article,
createdBy = citizen
)
}
}
override suspend fun getContent(call: ApplicationCall): OpinionArticle {
return call.receive<Content>().create(call.citizen, article)
}
}
/**
* Get all Opinion of citizen on targets by target ids
*/
@Location("/citizen/{citizen}/opinions")
class CitizenOpinions(val citizen: CitizenEntity, id: List<String>): KoinComponent {
@Location("/citizens/{citizen}/opinions")
class CitizenOpinions(val citizen: CitizenEntity, id: List<String>) : KoinComponent {
val id: List<UUID> = id.toUUID()
val opinionsEntities = get<OpinionArticleRepository>()
.findCitizenOpinionsByTargets(citizen, id.toUUID())
.findCitizenOpinionsByTargets(citizen, this.id)
}
}
@@ -64,9 +93,12 @@ fun Route.opinionArticle(repo: OpinionArticleRepository) {
}
put<OpinionArticlePaths.ArticleOpinion> {
val optionArticle = call.receive<ArticleOpinionRequest>().create(citizen)
assertCan(VIEW, optionArticle)
call.respond(HttpStatusCode.Created, optionArticle)
call.getContent(it)
.let { opinion ->
assertCan(VIEW, opinion)
repo.opinion(opinion)
}.let {
call.respond(HttpStatusCode.Created, it)
}
}
}

View File

@@ -1,7 +1,7 @@
package fr.dcproject.routes
import fr.dcproject.entity.OpinionChoice
import fr.dcproject.security.voter.OpinionVoter.Action.VIEW
import fr.dcproject.security.voter.OpinionChoiceVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -17,7 +17,7 @@ object OpinionChoicePaths {
class OpinionChoiceRequest(val opinionChoice: OpinionChoice)
@Location("/opinions")
class OpinionChoicesRequest(val targets: List<String>)
class OpinionChoicesRequest(val targets: List<String> = emptyList())
}
@KtorExperimentalLocationsAPI

View File

@@ -37,9 +37,10 @@ class OpinionVoter : Voter {
}
if (action == Action.DELETE) {
return if (subject is Opinion<*>
&& user != null
&& subject.createdBy.user.id == user.id)
return if (subject is Opinion<*> &&
user != null &&
subject.createdBy.user.id == user.id
)
Vote.GRANTED
else Vote.DENIED
}

View File

@@ -650,6 +650,123 @@ paths:
401:
$ref: '#/components/responses/401'
/articles/{article}/opinions:
parameters:
- $ref: '#/components/parameters/article'
put:
security:
- JWTAuth: []
summary: Add Opinion on one article
tags:
- opinion
- article
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleOpinionRequest'
responses:
201:
description: Return the opinion
content:
application/json:
schema:
$ref: '#/components/schemas/Opinion'
401:
$ref: '#/components/responses/401'
/opinions:
get:
summary: Get all opinions choices
tags:
- opinion
parameters:
- in: query
required: false
name: targets
description: opinion available for defined target
example:
- article
schema:
type: array
items:
type: string
responses:
200:
description: return
content:
application/json:
schema:
$ref: '#/components/schemas/OpinionChoices'
/opinions/{opinion}:
parameters:
- $ref: '#/components/parameters/opinion'
get:
summary: Get one opinion Choices
tags:
- opinion
responses:
200:
description: return
content:
application/json:
schema:
$ref: '#/components/schemas/OpinionChoice'
/citizens/{citizen}/opinions:
parameters:
- $ref: '#/components/parameters/citizen'
get:
security:
- JWTAuth: []
summary: Get all opinions of citizen filtered by target ids
tags:
- opinion
- citizen
parameters:
- in: query
required: true
name: id
description: target ids
example:
- 9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b
schema:
type: array
items:
type: string
format: uuid
responses:
200:
description: Opinions
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Opinion'
/citizens/{citizen}/opinions/articles:
parameters:
- $ref: '#/components/parameters/citizen'
get:
security:
- JWTAuth: []
summary: Get all opinions of one citizen
tags:
- opinion
- citizen
responses:
200:
description: Opinions
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Paginated'
- type: object
properties:
result:
type: array
items:
$ref: '#/components/schemas/Opinion'
/citizens/{citizen}/votes/articles:
parameters:
@@ -789,7 +906,7 @@ components:
name: citizen
in: path
description: ID of citizen
example: 4d673bfa-eaef-4290-b52f-85a9c8a7eba5
example: 6434f4f9-f570-f22a-c134-8668350651ff
required: true
schema:
type: string
@@ -804,6 +921,15 @@ components:
schema:
type: string
format: uuid
opinion:
in: path
required: true
name: opinion
description: Opinion ID
example: 6e978eb5-3c48-0def-b093-e01f43983adb
schema:
type: string
format: uuid
constitution:
name: constitution
@@ -861,6 +987,13 @@ components:
updated_at:
type: string
format: 'date-time'
DeletedAt:
properties:
deleted_at:
type: string
format: 'date-time'
deleted:
type: boolean
versionId:
properties:
@@ -1221,7 +1354,70 @@ components:
Opinion1: 1
Opinion2: 55
ArticleOpinionRequest:
type: object
properties:
opinion_choice:
type: string
format: uuid
example: 6e978eb5-3c48-0def-b093-e01f43983adb
OpinionChoices:
description: Opinion Choice
type: array
items:
$ref: '#/components/schemas/OpinionChoice'
OpinionChoice:
description: Opinion Choice
allOf:
- type: object
properties:
id:
type: string
format: uuid
name:
type: string
example: opinion1
target:
type: array
required: false
nullable: true
items:
type: string
description: the name of the target
- $ref: '#/components/schemas/CreatedAt'
- $ref: '#/components/schemas/DeletedAt'
Opinion:
description: Opinion
allOf:
- type: object
properties:
id:
type: string
format: uuid
name:
type: string
example: opinion1
target:
type: object
properties:
id:
type: string
format: uuid
reference:
type: string
example: article
choice:
type: object
allOf:
- $ref: '#/components/schemas/OpinionChoice'
reference:
type: string
example: opinion_on_article
- $ref: '#/components/schemas/CreatedBy'
- $ref: '#/components/schemas/CreatedAt'
@@ -1243,11 +1439,7 @@ components:
required: true
servers:
- description: localhost 80
url: http://localhost
- description: localhost 8080
- description: localhost
url: http://localhost:8080
- description: production
url: http://dc-project.fr
- description: SwaggerHub API Auto Mocking
url: https://virtserver.swaggerhub.com/flecomte/dc-project/0.1
url: http://dc-project.fr

View File

@@ -7,7 +7,10 @@ begin
delete from opinion_choice;
insert into opinion_choice (id, name, target)
select uuid_in(md5('opinion_choice'||row_number() over ())::cstring), 'Opinion'||row_number() over (), '{article}'
select
uuid_in(md5('opinion_choice'||row_number() over ())::cstring),
'Opinion'||row_number() over (),
case when row_number() over () % 5 = 0 then null else '{article}'::text[] end
from generate_series(0,20);
for i in 0..9 loop

View File

@@ -11,11 +11,17 @@ begin
find_article_by_id(_id)
when 'constitution'::regclass then
find_constitution_by_id(_id)
when 'comment'::regclass then
find_comment_by_id(_id)
when 'opinion'::regclass then
find_opinion_by_id(_id)
else
json_build_object('id', _id)
json_build_object('id', _id, 'reference', _reference)
end
into resource;
end;
resource = resource::jsonb || jsonb_build_object('reference', _reference);
end
$$;
-- drop function if exists find_reference_by_id(uuid, regclass, out json);

View File

@@ -18,6 +18,8 @@ begin
from (
select
o.*,
find_reference_by_id(o.target_id, o.target_reference) as target,
find_citizen_by_id(o.created_by_id) as created_by,
to_json(ol) as choice
from opinion as o
join opinion_choice ol on o.choice_id = ol.id

View File

@@ -11,6 +11,8 @@ begin
from (
select
o.*,
find_reference_by_id(o.target_id, o.target_reference) as target,
find_citizen_by_id(o.created_by_id) as created_by,
to_json(ol) as choice
from opinion as o
join opinion_choice ol on o.choice_id = ol.id

View File

@@ -9,6 +9,8 @@ begin
from (
select
o.*,
find_reference_by_id(o.target_id, o.target_reference) as target,
find_citizen_by_id(o.created_by_id) as created_by,
to_json(ol) as choice
from opinion as o
join opinion_choice ol on o.choice_id = ol.id

View File

@@ -0,0 +1,22 @@
create or replace function find_opinion_by_id(
_id uuid,
out resource json
) language plpgsql as
$$
begin
select to_json(t)
into resource
from (
select
o.*,
find_reference_by_id(o.target_id, o.target_reference) as target,
find_citizen_by_id(o.created_by_id) as created_by,
to_json(ol) as choice
from "opinion" as o
join opinion_choice ol on o.choice_id = ol.id
where o.id = _id
) as t;
end;
$$;
-- drop function if exists find_opinion_by_id(uuid, out json);

View File

@@ -0,0 +1,27 @@
create or replace function find_opinion_by_opinion(
inout resource json
) language plpgsql as
$$
declare
_target_id uuid = (resource#>>'{target, id}')::uuid;
_created_by_id uuid = (resource#>>'{created_by, id}')::uuid;
_choice_id uuid = (resource#>>'{choice, id}')::uuid;
begin
select to_json(t)
into resource
from (
select
o.*,
find_reference_by_id(o.target_id, o.target_reference) as target,
find_citizen_by_id(o.created_by_id) as created_by,
to_json(ol) as choice
from "opinion" as o
join opinion_choice ol on o.choice_id = ol.id
where o.target_id = _target_id
and o.created_by_id = _created_by_id
and o.choice_id = _choice_id
) as t;
end;
$$;
-- drop function if exists find_opinion_by_opinion(json);

View File

@@ -8,7 +8,11 @@ begin
select ol.*
from opinion_choice ol
where (ol.deleted_at is null or ol.deleted_at > now())
and (ol.target is null or targets is null or array_length(targets, 1) = 0 or ol.target && targets)
and (
ol.target is null or array_length(ol.target, 1) is null -- if choice is compatible with all target
or targets is null or array_length(targets, 1) is null -- if no target defined
or (ol.target && targets) -- if target is compatible
)
order by ol.name
) t;
@@ -17,4 +21,4 @@ $$;
-- drop function if exists find_opinions();
-- select find_opinions();
-- select find_opinion_choices('{}');

View File

@@ -1,17 +0,0 @@
create or replace function opinion(reference regclass, _target_id uuid, _created_by_id uuid, _opinion uuid, out resource json)
language plpgsql as
$$
begin
if reference = 'article'::regclass then
insert into opinion_on_article (created_by_id, target_id, choice_id)
values (_created_by_id, _target_id, _opinion)
on conflict (created_by_id, target_id, choice_id) do nothing;
else
raise exception '% no implemented for opinion', reference::text;
end if;
select count_opinion(_target_id) into resource;
end;
$$;
-- drop function if exists vote(regclass,uuid,uuid,integer,boolean);

View File

@@ -0,0 +1,23 @@
create or replace function upsert_opinion(inout resource json)
language plpgsql as
$$
declare
_reference regclass = (resource#>>'{target, reference}')::regclass;
_id uuid = coalesce((resource->>'id')::uuid, uuid_generate_v4());
_target_id uuid = (resource#>>'{target, id}')::uuid;
_created_by_id uuid = (resource#>>'{created_by, id}')::uuid;
_choice_id uuid = (resource#>>'{choice, id}')::uuid;
begin
if _reference = 'article'::regclass then
insert into opinion_on_article (id, created_by_id, target_id, choice_id)
values (_id, _created_by_id, _target_id, _choice_id)
on conflict (created_by_id, target_id, choice_id) do nothing;
else
raise exception '% no implemented for opinion', _reference::text;
end if;
select find_opinion_by_opinion(resource) into resource;
end;
$$;
-- drop function if exists upsert_opinion(json);

View File

@@ -7,6 +7,7 @@ import fr.dcproject.module
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.server.testing.withTestApplication
import io.ktor.util.KtorExperimentalAPI
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.koin.test.AutoCloseKoinTest
@@ -18,6 +19,7 @@ import org.koin.test.get
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MailerTest : KoinTest, AutoCloseKoinTest() {
@Test
@Tag("online")
fun `can be send an email`() {
withTestApplication({ module(Env.TEST) }) {
get<Mailer>().sendEmail {

View File

@@ -6,8 +6,8 @@ import fr.dcproject.utils.LoggerDelegate
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
import io.cucumber.core.api.Scenario
import io.cucumber.java8.En
import io.cucumber.java8.Scenario
import io.cucumber.junit.Cucumber
import io.cucumber.junit.CucumberOptions
import io.ktor.locations.KtorExperimentalLocationsAPI

View File

@@ -2,6 +2,8 @@ package feature
import fr.dcproject.entity.*
import fr.dcproject.repository.CommentArticle
import fr.dcproject.utils.toUUID
import io.cucumber.datatable.DataTable
import io.cucumber.java8.En
import org.joda.time.DateTime
import org.koin.test.KoinTest
@@ -16,6 +18,9 @@ import fr.dcproject.repository.Citizen as CitizenRepository
class ArticleSteps : En, KoinTest {
init {
/**
* @deprecated
*/
Given("I have article with id {string}") { id: String ->
var citizen = Citizen(
name = CitizenI.Name("John", "Doe"),
@@ -40,6 +45,23 @@ class ArticleSteps : En, KoinTest {
get<ArticleRepository>().upsert(article)
}
Given("I have article") { extraData: DataTable ->
extraData.asMap<String, String>(String::class.java, String::class.java).let { params ->
val username = params["createdBy"]?.toLowerCase()?.replace(' ', '-') ?: error("You must provide the 'createdBy' parameter")
val citizen = get<CitizenRepository>().findByUsername(username) ?: error("Citizen not exist")
val id = params["id"]?.toUUID() ?: UUID.randomUUID()
val article = ArticleEntity(
id = id,
title = "hello",
content = "bla bla bla",
description = "A super article",
createdBy = citizen
)
get<ArticleRepository>().upsert(article)
}
}
Given("I have article with id {string} created by {string}") { id: String, username: String ->
val citizen = get<CitizenRepository>().findByUsername(username)!!

View File

@@ -5,6 +5,7 @@ import fr.dcproject.JwtConfig
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.User
import fr.dcproject.utils.toUUID
import fr.postgresjson.connexion.Requester
import io.cucumber.datatable.DataTable
import io.cucumber.java8.En
@@ -70,6 +71,38 @@ class KtorServerAuthSteps : En, KoinTest {
}
}
Given("I have citizen {word} {word}") { firstName: String, lastName: String, extraInfo: DataTable? ->
val id: UUID = extraInfo?.asMap<String, String>(String::class.java, String::class.java)?.get("id")?.toUUID() ?: UUID.randomUUID()
val user = User(
id = id,
username = "$firstName-$lastName".toLowerCase(),
plainPassword = "azerty"
)
val citizen = Citizen(
id = id,
name = CitizenI.Name(firstName, lastName),
email = ("$firstName-$lastName".toLowerCase()) + "@dc-project.fr",
birthday = DateTime.now(),
user = user
)
get<CitizenRepository>().insertWithUser(citizen)
}
Given("I am authenticated as {word} {word}") { firstName: String, lastName: String ->
val username = "$firstName-$lastName".toLowerCase()
val citizen = get<CitizenRepository>().findByUsername(username) ?: error("Cititzen not exist with username $username")
val jwtAsString: String = JWT.create()
.withIssuer("dc-project.fr")
.withClaim("id", citizen.id.toString())
.sign(JwtConfig.algorithm)
KtorServerContext.defaultServer.addPreRequestSetup {
addHeader(HttpHeaders.Authorization, "Bearer $jwtAsString")
}
}
Given("I have citizen {word} {word} with id {string}") { firstName: String, lastName: String, id: String ->
val user = User(
id = UUID.randomUUID(),

View File

@@ -41,20 +41,20 @@ class KtorServerRestSteps : En {
}
}
private fun findJsonElement(node: String): JsonElement {
private fun findJsonElement(path: String): JsonElement {
var jsonElement: JsonElement = responseJsonElement
val elements = node.split(".")
elements.forEach {
val asArrayIndex = """\d+""".toRegex().find(it)
jsonElement = if (asArrayIndex != null) {
val index = asArrayIndex.groups.first()!!
jsonElement.jsonArray.get(index.value.toInt())
} else {
jsonElement.jsonObject.get(it) ?: throw AssertionError("\"$node\" element not found on json response")
path
.split("].", "]", "[", ".")
.filter { it.trim().isNotBlank() }
.map { it.trim() }
.forEach {
jsonElement = if (jsonElement is JsonArray) {
jsonElement.jsonArray[it.toInt()]
} else {
jsonElement.jsonObject[it]
} ?: throw AssertionError("\"$path\" element not found on json response")
}
}
return jsonElement
}

View File

@@ -0,0 +1,40 @@
package feature
import fr.dcproject.entity.OpinionArticle
import fr.dcproject.utils.toUUID
import io.cucumber.datatable.DataTable
import io.cucumber.java8.En
import org.koin.test.KoinTest
import org.koin.test.get
import fr.dcproject.repository.Article as ArticleRepository
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.OpinionArticle as OpinionRepository
import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository
class OpinionSteps : En, KoinTest {
init {
Given("I have the opinion {string} on article {string} created by {string}:") { opinionChoice: String, article: String, citizen: String, extraInfo: DataTable ->
extraInfo.asMap<String, String>(String::class.java, String::class.java).let {
val opinion = OpinionArticle(
choice = get<OpinionChoiceRepository>().findOpinionsChoiceByName(opinionChoice) ?: error("Opinion Choice not exist"),
target = get<ArticleRepository>().findById(article.toUUID()) ?: error("Article not exist"),
createdBy = get<CitizenRepository>().findById(citizen.toUUID()) ?: error("Citizen not exist")
)
get<OpinionRepository>().opinion(opinion)
}
}
Given("I have an opinion") { extraInfo: DataTable ->
extraInfo.asMap<String, String>(String::class.java, String::class.java)?.let { params ->
val username = params["createdBy"]?.toLowerCase()?.replace(' ', '-') ?: error("You must provide the 'createdBy' parameter")
val opinion = OpinionArticle(
choice = params["opinion"]?.let { get<OpinionChoiceRepository>().findOpinionsChoiceByName(it) ?: error("Opinion Choice not exist")} ?: error("You must provide the 'opinion' parameter"),
target = params["article"]?.let { get<ArticleRepository>().findById(it.toUUID()) ?: error("Article not exist")} ?: error("You must provide the 'article' parameter"),
createdBy = get<CitizenRepository>().findByUsername(username) ?: error("Citizen not exist")
)
get<OpinionRepository>().opinion(opinion)
}
}
}
}

View File

@@ -19,6 +19,7 @@ Feature: citizens routes
And the response should contain object:
| id | 64b7b379-2298-43ec-b428-ba134930cabd |
@online
Scenario: Can be connect with SSO
Given I have citizen:
| id | c606110c-ff0e-4d09-a79e-74632d7bf7bd |

View File

@@ -0,0 +1,59 @@
@opinion
Feature: Opinion
Scenario: Can get one opinion Choices
When I send a GET request to "/opinions/6e978eb5-3c48-0def-b093-e01f43983adb"
Then the response status code should be 200
And the JSON should contain:
| name | Opinion1 |
Scenario: Can get all opinion choices
When I send a GET request to "/opinions"
Then the response status code should be 200
And the JSON should contain:
| [0]name | Opinion1 |
Scenario: Can create opinion on article
Given I have citizen Isaac Newton
| id | 2f414045-95d9-42ca-a3a9-8cdde52ad253 |
And I am authenticated as Isaac Newton
And I have article
| id | 9226c1a3-8091-c3fa-7d0d-c2e98c9bee7 |
| createdBy | Isaac Newton |
And I have an opinion
| opinion | Opinion1 |
| article | 9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b |
| createdBy | Isaac Newton |
When I send a PUT request to "/articles/9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b/opinions" with body:
"""
{
"opinion_choice": "6e978eb5-3c48-0def-b093-e01f43983adb"
}
"""
Then the response status code should be 201
Scenario: Can I get all opinions of citizen filtered by target ids
When I send a GET request to "/citizens/6434f4f9-f570-f22a-c134-8668350651ff/opinions?id=9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b"
Then the response status code should be 200
And the JSON should contain:
| [0].name | Opinion2 |
Scenario: Can recieve opinion aggregation with article
When I send a GET request to "/articles/9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b"
Then the response status code should be 200
And the JSON should contain:
| opinions.Opinion2 | 1 |
Scenario: Can get all opinion of one citizen
Given I have citizen Albert Einstein
| id | c1542096-3431-432d-8e35-9dc071d4c818 |
And I am authenticated as Albert Einstein
And I have an opinion
| opinion | Opinion1 |
| article | 9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b |
| createdBy | Albert Einstein |
When I send a GET request to "/citizens/c1542096-3431-432d-8e35-9dc071d4c818/opinions/articles"
Then the response status code should be 200
And the JSON element result should have 1 items
And the JSON should contain:
| result[0].name | Opinion1 |

View File

@@ -38,8 +38,9 @@ declare
"draft":false
}
$json$;
opinion1 uuid = uuid_generate_v4();
opinion2 uuid = uuid_generate_v4();
opinion_choice1_id uuid = uuid_generate_v4();
opinion_choice2_id uuid = uuid_generate_v4();
opinion2 json;
begin
-- insert user for context
select insert_user(created_user) into created_user;
@@ -59,32 +60,38 @@ begin
insert into opinion_choice(id, name, target)
values (opinion1, 'Opinion1', '{article}');
values (opinion_choice1_id, 'Opinion1', '{article}');
insert into opinion_choice(id, name, target)
values (opinion2, 'Opinion2', '{article}');
insert into opinion_choice(id, name)
values (opinion_choice2_id, 'Opinion2');
insert into opinion_choice(name, target)
values ('Opinion3', '{article}');
perform opinion(
reference => 'article'::regclass,
_target_id => (created_article->>'id')::uuid,
_created_by_id => _citizen_id,
_opinion => opinion1
);
perform opinion(
reference => 'article'::regclass,
_target_id => (created_article->>'id')::uuid,
_created_by_id => _citizen_id,
_opinion => opinion2
perform upsert_opinion(
resource => json_build_object(
'target', json_build_object('id', (created_article->'id'), 'reference', 'article'),
'created_by', json_build_object('id', _citizen_id),
'choice', json_build_object('id', opinion_choice1_id)
)
);
select upsert_opinion(
resource => json_build_object(
'target', json_build_object('id', (created_article->'id'), 'reference', 'article'),
'created_by', json_build_object('id', _citizen_id),
'choice', json_build_object('id', opinion_choice2_id)
)
) into opinion2;
assert (select count(*) = 2 from opinion_on_article), 'opinions must be inserted';
assert (select choice_id = opinion1 from opinion_on_article limit 1), 'opinion must be inserted';
assert (select choice_id = opinion_choice1_id from opinion_on_article limit 1), 'opinion must be inserted';
assert(select (a#>>'{opinions, Opinion1}')::int = 1
from find_article_by_id((created_article->>'id')::uuid) a), 'the article must be have a opinion';
raise notice '%', opinion2;
assert(select (opinion2#>>'{choice, id}')::uuid = opinion_choice2_id), 'opinion2 is not inserted';
assert(select (opinion2#>>'{choice, name}') = 'Opinion2'), 'no name for opinion2';
assert(
select (o#>>'{0, choice, name}') = 'Opinion1'
from find_citizen_opinions_by_target_id(_citizen_id, (created_article->>'id')::uuid) o),
@@ -100,7 +107,11 @@ begin
), 'find_opinion_choices must be return all opinions';
assert(
select (find_opinion_choice_by_id(opinion1)->>'name') = 'Opinion1'
select find_opinion_choices('{}')#>>'{0, name}' = 'Opinion1'
), 'find_opinion_choices must be return all opinions if no target is defined';
assert(
select (find_opinion_choice_by_id(opinion_choice1_id)->>'name') = 'Opinion1'
), 'find_opinion_choice_by_id must return the opinion_choice';
assert(