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"> <component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" /> <option name="languageLevel" value="ES6" />
</component> </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" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View File

@@ -1,14 +1,11 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests + Lint" type="JUnit" factoryName="JUnit" show_console_on_std_err="true"> <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" /> <output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly /> <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="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" /> <option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_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="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="TEST_SEARCH_SCOPE"> <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"> <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="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="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> </method>
</configuration> </configuration>
</component> </component>

View File

@@ -32,7 +32,7 @@
</extension> </extension>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled> <GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2"> <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> </method>
</configuration> </configuration>
</component> </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/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/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/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/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/check_user.sql" />
<option value="$PROJECT_DIR$/src/main/resources/sql/functions/user/find_user_by_id.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.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_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_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_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/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/user.sql" />
<option value="$PROJECT_DIR$/src/test/sql/citizen.sql" /> <option value="$PROJECT_DIR$/src/test/sql/citizen.sql" />
<option value="$PROJECT_DIR$/src/test/sql/article.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 postgresjson_version=0.1
koinVersion=2.0.1 koinVersion=2.0.1
jackson_version=2.9.9 jackson_version=2.9.9
cucumber_version=4.7.1 cucumber_version=5.1.3
systemProp.sonar.host.url=http://localhost:9000 systemProp.sonar.host.url=http://localhost:9000
systemProp.sonar.login=1196e8015c20035f1aa91e881b95ce9d6e879c8a systemProp.sonar.login=1196e8015c20035f1aa91e881b95ce9d6e879c8a

View File

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

View File

@@ -32,8 +32,6 @@ open class Comment<T : TargetI>(
target = parent.target, target = parent.target,
content = content content = content
) )
override val reference get() = TargetI.getReference(this)
} }
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentS(id) 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 fr.postgresjson.entity.immutable.UuidEntityI
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf import kotlin.reflect.full.isSubclassOf
interface ExtraI<T : TargetI> : interface ExtraI<T : TargetI> :
UuidEntityI, UuidEntityI,
@@ -26,16 +26,18 @@ interface TargetI : UuidEntityI {
enum class TargetName(val targetReference: String) { enum class TargetName(val targetReference: String) {
Article("article"), Article("article"),
Constitution("constitution"), Constitution("constitution"),
Comment("comment") Comment("comment"),
Opinion("opinion")
} }
companion object { companion object {
fun <T : TargetI> getReference(t: KClass<T>): String { fun <T : TargetI> getReference(t: KClass<T>): String {
return when { return when {
t.isSuperclassOf(Article::class) -> TargetName.Article.targetReference t.isSubclassOf(ArticleRef::class) -> TargetName.Article.targetReference
t.isSuperclassOf(Constitution::class) -> TargetName.Constitution.targetReference t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSuperclassOf(Comment::class) -> TargetName.Comment.targetReference t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
else -> throw error("target not implemented") 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 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.* import java.util.*
open class Opinion<T : TargetI>( open class Opinion<T : TargetI>(
@@ -9,11 +12,16 @@ open class Opinion<T : TargetI>(
override val target: T, override val target: T,
val choice: OpinionChoice val choice: OpinionChoice
) : ExtraI<T>, ) : ExtraI<T>,
UuidEntity(id), TargetRef(id),
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy) { EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy) {
fun getName(): String = choice.name 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( class OpinionChoice(
id: UUID, id: UUID,
val name: String, val name: String,
val target: List<String> val target: List<String>?
) : OpinionChoiceRef(id), ) : OpinionChoiceRef(id),
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp() EntityDeletedAt by EntityDeletedAtImp()

View File

@@ -3,13 +3,13 @@ package fr.dcproject.entity
import fr.postgresjson.entity.EntityI import fr.postgresjson.entity.EntityI
class OpinionAggregation( class OpinionAggregation(
override val entries: Set<Map.Entry<String, Int>> = emptySet() private val underlying: MutableMap<String, Any> = mutableMapOf()
) : AbstractMap<String, Int>(), EntityI ) : MutableMap<String, Any> by underlying, EntityI
interface Opinionable { interface Opinionable {
val opinions: MutableMap<String, Int> var opinions: MutableMap<String, Int>
} }
class OpinionableImp : Opinionable { 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 package fr.dcproject.entity.request
import fr.dcproject.entity.CitizenRef import io.ktor.application.ApplicationCall
import fr.postgresjson.entity.EntityI
interface Request interface Request
interface RequestBuilder<E: EntityI> : Request { interface RequestBuilder<E> {
fun create(): E suspend fun getContent(call: ApplicationCall): E
} }
interface RequestBuilderWithCreator<C: CitizenRef, E: EntityI> : Request { suspend fun <E> ApplicationCall.getContent(builder: RequestBuilder<E>) = builder.getContent(this)
fun create(citizen: C): E
}

View File

@@ -11,6 +11,7 @@ import net.pearx.kasechange.toSnakeCase
import java.util.* import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Opinion as OpinionEntity import fr.dcproject.entity.Opinion as OpinionEntity
import fr.dcproject.entity.OpinionArticle as OpinionArticleEntity
import fr.dcproject.entity.OpinionChoice as OpinionChoiceEntity import fr.dcproject.entity.OpinionChoice as OpinionChoiceEntity
open class OpinionChoice(override val requester: Requester) : RepositoryI { open class OpinionChoice(override val requester: Requester) : RepositoryI {
@@ -25,6 +26,14 @@ open class OpinionChoice(override val requester: Requester) : RepositoryI {
"targets" to targets "targets" to targets
) )
/**
* find opinion choices by name
*/
fun findOpinionsChoiceByName(name: String): OpinionChoiceEntity? =
findOpinionsChoices().first {
it.name == name
}
/** /**
* find one opinion choices by id * find one opinion choices by id
*/ */
@@ -104,9 +113,18 @@ open class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requeste
.select(page, limit, .select(page, limit,
"sort" to sort?.toSnakeCase(), "sort" to sort?.toSnakeCase(),
"direction" to direction, "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 package fr.dcproject.routes
import fr.dcproject.citizen 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.OpinionVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan import fr.dcproject.security.voter.assertCan
import fr.dcproject.utils.toUUID import fr.dcproject.utils.toUUID
import io.ktor.application.ApplicationCall
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.BadRequestException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
@@ -17,6 +24,7 @@ import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.get import org.koin.core.get
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Citizen as CitizenEntity import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
@@ -24,7 +32,7 @@ import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object OpinionArticlePaths { object OpinionArticlePaths {
/** /**
* Get paginated opinion of citizen for one article * Get paginated opinions of citizen for all articles
*/ */
@Location("/citizens/{citizen}/opinions/articles") @Location("/citizens/{citizen}/opinions/articles")
class CitizenOpinionArticleRequest( class CitizenOpinionArticleRequest(
@@ -36,16 +44,37 @@ object OpinionArticlePaths {
/** /**
* Put an opinion on one article * Put an opinion on one article
*/ */
@Location("/articles/{article}/opinons") @Location("/articles/{article}/opinions")
class ArticleOpinion(val article: ArticleEntity) @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 * Get all Opinion of citizen on targets by target ids
*/ */
@Location("/citizen/{citizen}/opinions") @Location("/citizens/{citizen}/opinions")
class CitizenOpinions(val citizen: CitizenEntity, id: List<String>): KoinComponent { class CitizenOpinions(val citizen: CitizenEntity, id: List<String>) : KoinComponent {
val id: List<UUID> = id.toUUID()
val opinionsEntities = get<OpinionArticleRepository>() val opinionsEntities = get<OpinionArticleRepository>()
.findCitizenOpinionsByTargets(citizen, id.toUUID()) .findCitizenOpinionsByTargets(citizen, this.id)
} }
} }
@@ -64,9 +93,12 @@ fun Route.opinionArticle(repo: OpinionArticleRepository) {
} }
put<OpinionArticlePaths.ArticleOpinion> { put<OpinionArticlePaths.ArticleOpinion> {
val optionArticle = call.receive<ArticleOpinionRequest>().create(citizen) call.getContent(it)
assertCan(VIEW, optionArticle) .let { opinion ->
assertCan(VIEW, opinion)
call.respond(HttpStatusCode.Created, optionArticle) repo.opinion(opinion)
}.let {
call.respond(HttpStatusCode.Created, it)
}
} }
} }

View File

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

View File

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

View File

@@ -650,6 +650,123 @@ paths:
401: 401:
$ref: '#/components/responses/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: /citizens/{citizen}/votes/articles:
parameters: parameters:
@@ -789,7 +906,7 @@ components:
name: citizen name: citizen
in: path in: path
description: ID of citizen description: ID of citizen
example: 4d673bfa-eaef-4290-b52f-85a9c8a7eba5 example: 6434f4f9-f570-f22a-c134-8668350651ff
required: true required: true
schema: schema:
type: string type: string
@@ -804,6 +921,15 @@ components:
schema: schema:
type: string type: string
format: uuid format: uuid
opinion:
in: path
required: true
name: opinion
description: Opinion ID
example: 6e978eb5-3c48-0def-b093-e01f43983adb
schema:
type: string
format: uuid
constitution: constitution:
name: constitution name: constitution
@@ -861,6 +987,13 @@ components:
updated_at: updated_at:
type: string type: string
format: 'date-time' format: 'date-time'
DeletedAt:
properties:
deleted_at:
type: string
format: 'date-time'
deleted:
type: boolean
versionId: versionId:
properties: properties:
@@ -1221,7 +1354,70 @@ components:
Opinion1: 1 Opinion1: 1
Opinion2: 55 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 required: true
servers: servers:
- description: localhost 80 - description: localhost
url: http://localhost
- description: localhost 8080
url: http://localhost:8080 url: http://localhost:8080
- description: production - description: production
url: http://dc-project.fr url: http://dc-project.fr
- description: SwaggerHub API Auto Mocking
url: https://virtserver.swaggerhub.com/flecomte/dc-project/0.1

View File

@@ -7,7 +7,10 @@ begin
delete from opinion_choice; delete from opinion_choice;
insert into opinion_choice (id, name, target) 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); from generate_series(0,20);
for i in 0..9 loop for i in 0..9 loop

View File

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

View File

@@ -18,6 +18,8 @@ begin
from ( from (
select select
o.*, 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 to_json(ol) as choice
from opinion as o from opinion as o
join opinion_choice ol on o.choice_id = ol.id join opinion_choice ol on o.choice_id = ol.id

View File

@@ -11,6 +11,8 @@ begin
from ( from (
select select
o.*, 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 to_json(ol) as choice
from opinion as o from opinion as o
join opinion_choice ol on o.choice_id = ol.id join opinion_choice ol on o.choice_id = ol.id

View File

@@ -9,6 +9,8 @@ begin
from ( from (
select select
o.*, 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 to_json(ol) as choice
from opinion as o from opinion as o
join opinion_choice ol on o.choice_id = ol.id 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.* select ol.*
from opinion_choice ol from opinion_choice ol
where (ol.deleted_at is null or ol.deleted_at > now()) 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 order by ol.name
) t; ) t;
@@ -17,4 +21,4 @@ $$;
-- drop function if exists find_opinions(); -- 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.locations.KtorExperimentalLocationsAPI
import io.ktor.server.testing.withTestApplication import io.ktor.server.testing.withTestApplication
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.koin.test.AutoCloseKoinTest import org.koin.test.AutoCloseKoinTest
@@ -18,6 +19,7 @@ import org.koin.test.get
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MailerTest : KoinTest, AutoCloseKoinTest() { class MailerTest : KoinTest, AutoCloseKoinTest() {
@Test @Test
@Tag("online")
fun `can be send an email`() { fun `can be send an email`() {
withTestApplication({ module(Env.TEST) }) { withTestApplication({ module(Env.TEST) }) {
get<Mailer>().sendEmail { get<Mailer>().sendEmail {

View File

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

View File

@@ -2,6 +2,8 @@ package feature
import fr.dcproject.entity.* import fr.dcproject.entity.*
import fr.dcproject.repository.CommentArticle import fr.dcproject.repository.CommentArticle
import fr.dcproject.utils.toUUID
import io.cucumber.datatable.DataTable
import io.cucumber.java8.En import io.cucumber.java8.En
import org.joda.time.DateTime import org.joda.time.DateTime
import org.koin.test.KoinTest import org.koin.test.KoinTest
@@ -16,6 +18,9 @@ import fr.dcproject.repository.Citizen as CitizenRepository
class ArticleSteps : En, KoinTest { class ArticleSteps : En, KoinTest {
init { init {
/**
* @deprecated
*/
Given("I have article with id {string}") { id: String -> Given("I have article with id {string}") { id: String ->
var citizen = Citizen( var citizen = Citizen(
name = CitizenI.Name("John", "Doe"), name = CitizenI.Name("John", "Doe"),
@@ -40,6 +45,23 @@ class ArticleSteps : En, KoinTest {
get<ArticleRepository>().upsert(article) 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 -> Given("I have article with id {string} created by {string}") { id: String, username: String ->
val citizen = get<CitizenRepository>().findByUsername(username)!! 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.Citizen
import fr.dcproject.entity.CitizenI import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.User import fr.dcproject.entity.User
import fr.dcproject.utils.toUUID
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import io.cucumber.datatable.DataTable import io.cucumber.datatable.DataTable
import io.cucumber.java8.En 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 -> Given("I have citizen {word} {word} with id {string}") { firstName: String, lastName: String, id: String ->
val user = User( val user = User(
id = UUID.randomUUID(), 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 var jsonElement: JsonElement = responseJsonElement
val elements = node.split(".")
elements.forEach { path
val asArrayIndex = """\d+""".toRegex().find(it) .split("].", "]", "[", ".")
.filter { it.trim().isNotBlank() }
jsonElement = if (asArrayIndex != null) { .map { it.trim() }
val index = asArrayIndex.groups.first()!! .forEach {
jsonElement.jsonArray.get(index.value.toInt()) jsonElement = if (jsonElement is JsonArray) {
} else { jsonElement.jsonArray[it.toInt()]
jsonElement.jsonObject.get(it) ?: throw AssertionError("\"$node\" element not found on json response") } else {
jsonElement.jsonObject[it]
} ?: throw AssertionError("\"$path\" element not found on json response")
} }
}
return jsonElement 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: And the response should contain object:
| id | 64b7b379-2298-43ec-b428-ba134930cabd | | id | 64b7b379-2298-43ec-b428-ba134930cabd |
@online
Scenario: Can be connect with SSO Scenario: Can be connect with SSO
Given I have citizen: Given I have citizen:
| id | c606110c-ff0e-4d09-a79e-74632d7bf7bd | | 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 "draft":false
} }
$json$; $json$;
opinion1 uuid = uuid_generate_v4(); opinion_choice1_id uuid = uuid_generate_v4();
opinion2 uuid = uuid_generate_v4(); opinion_choice2_id uuid = uuid_generate_v4();
opinion2 json;
begin begin
-- insert user for context -- insert user for context
select insert_user(created_user) into created_user; select insert_user(created_user) into created_user;
@@ -59,32 +60,38 @@ begin
insert into opinion_choice(id, name, target) insert into opinion_choice(id, name, target)
values (opinion1, 'Opinion1', '{article}'); values (opinion_choice1_id, 'Opinion1', '{article}');
insert into opinion_choice(id, name, target) insert into opinion_choice(id, name)
values (opinion2, 'Opinion2', '{article}'); values (opinion_choice2_id, 'Opinion2');
insert into opinion_choice(name, target) insert into opinion_choice(name, target)
values ('Opinion3', '{article}'); values ('Opinion3', '{article}');
perform opinion( perform upsert_opinion(
reference => 'article'::regclass, resource => json_build_object(
_target_id => (created_article->>'id')::uuid, 'target', json_build_object('id', (created_article->'id'), 'reference', 'article'),
_created_by_id => _citizen_id, 'created_by', json_build_object('id', _citizen_id),
_opinion => opinion1 'choice', json_build_object('id', opinion_choice1_id)
); )
perform opinion(
reference => 'article'::regclass,
_target_id => (created_article->>'id')::uuid,
_created_by_id => _citizen_id,
_opinion => opinion2
); );
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 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 assert(select (a#>>'{opinions, Opinion1}')::int = 1
from find_article_by_id((created_article->>'id')::uuid) a), 'the article must be have a opinion'; 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( assert(
select (o#>>'{0, choice, name}') = 'Opinion1' select (o#>>'{0, choice, name}') = 'Opinion1'
from find_citizen_opinions_by_target_id(_citizen_id, (created_article->>'id')::uuid) o), 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'; ), 'find_opinion_choices must be return all opinions';
assert( 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'; ), 'find_opinion_choice_by_id must return the opinion_choice';
assert( assert(