Merge pull request #91 from flecomte/21-valid-input

Valider les resource entrente
This commit was merged in pull request #91.
This commit is contained in:
2021-04-16 03:27:10 +02:00
committed by GitHub
83 changed files with 2336 additions and 495 deletions

View File

@@ -69,6 +69,13 @@ jobs:
with: with:
name: Build name: Build
path: build path: build
- name: Composer Up
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: testSqlComposeUp
- name: TestSql - name: TestSql
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
with: with:
@@ -81,19 +88,29 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 11
- uses: actions/download-artifact@v2 - uses: actions/download-artifact@v2
with: with:
name: Build name: Build
path: build path: build
- name: Composer Up
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: testComposeUp
- name: Test - name: Test
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: 6.8 gradle-version: 6.8
arguments: test -x testSql arguments: test
- name: Coverage - name: Coverage
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
with: with:
@@ -101,17 +118,28 @@ jobs:
arguments: coveralls arguments: coveralls
env: env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
- name: Cache SonarCloud packages - name: Cache SonarCloud packages
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
path: ~/.sonar/cache path: ~/.sonar/cache
key: ${{ runner.os }}-sonar key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar
- name: Test
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: test
- name: Build and analyze - name: Build and analyze
uses: eskatos/gradle-command-action@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew build sonarqube --info with:
gradle-version: 6.8
arguments: sonarqube --info
lint: lint:
needs: build needs: build

View File

@@ -4,7 +4,7 @@
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck" /> <option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt" />
<option name="taskDescriptions"> <option name="taskDescriptions">
<list /> <list />
</option> </option>

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube without test" type="GradleRunConfiguration" factoryName="Gradle"> <configuration default="false" name="Sonarqube (Send without run test)" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test With Dependencies" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="testWithDependencies" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,5 @@
# DC Project # DC Project
[![CodeFactor](https://www.codefactor.io/repository/github/flecomte/dc-project/badge?s=869dc426625a253a07bea95f9380e23fdb048b94)](https://www.codefactor.io/repository/github/flecomte/dc-project)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/0ec4fe63370148ca956974f90f8d55be)](https://www.codacy.com/gh/flecomte/dc-project/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=flecomte/dc-project&amp;utm_campaign=Badge_Grade)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=dc-project) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=dc-project)
[![Tests](https://github.com/flecomte/dc-project/actions/workflows/tests.yml/badge.svg)](https://github.com/flecomte/dc-project/actions/workflows/tests.yml) [![Tests](https://github.com/flecomte/dc-project/actions/workflows/tests.yml/badge.svg)](https://github.com/flecomte/dc-project/actions/workflows/tests.yml)

View File

@@ -94,7 +94,7 @@ val migration by tasks.registering {
} }
val migrationTest by tasks.registering { val migrationTest by tasks.registering {
group = "verification" group = "tests"
dependsOn(tasks.named("testComposeUp")) dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown")) finalizedBy(tasks.named("testComposeDown"))
doLast { doLast {
@@ -118,11 +118,9 @@ val migrationTest by tasks.registering {
} }
val testSql by tasks.registering { val testSql by tasks.registering {
group = "verification" group = "tests"
dependsOn(tasks.named("processResources")) dependsOn(tasks.named("processResources"))
dependsOn(tasks.named("processTestResources")) dependsOn(tasks.named("processTestResources"))
dependsOn(tasks.named("testSqlComposeUp"))
finalizedBy(tasks.named("testSqlComposeDown"))
doLast { doLast {
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve() val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
@@ -183,8 +181,6 @@ tasks.named<ShadowJar>("shadowJar") {
} }
tasks.sonarqube.configure { tasks.sonarqube.configure {
dependsOn(tasks.test)
dependsOn(tasks.detekt)
dependsOn(tasks.jacocoTestReport) dependsOn(tasks.jacocoTestReport)
} }
@@ -198,7 +194,6 @@ tasks.test {
useJUnit() useJUnit()
useJUnitPlatform() useJUnitPlatform()
systemProperty("junit.jupiter.execution.parallel.enabled", true) systemProperty("junit.jupiter.execution.parallel.enabled", true)
dependsOn(testSql)
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
} }
@@ -206,13 +201,6 @@ coveralls {
sourceDirs.add("src/main/kotlin") sourceDirs.add("src/main/kotlin")
} }
tasks.register("testAll") {
group = "verification"
dependsOn(testSql)
dependsOn(tasks.test)
dependsOn(tasks.ktlintCheck)
}
apply(plugin = "docker-compose") apply(plugin = "docker-compose")
dockerCompose { dockerCompose {
projectName = "dc-project" projectName = "dc-project"
@@ -228,14 +216,12 @@ dockerCompose {
useComposeFiles = listOf("docker-compose-test.yml") useComposeFiles = listOf("docker-compose-test.yml")
startedServices = listOf("db", "elasticsearch") startedServices = listOf("db", "elasticsearch")
stopContainers = false stopContainers = false
isRequiredBy(project.tasks.named("testSql"))
} }
createNested("test").apply { createNested("test").apply {
projectName = "dc-project_test" projectName = "dc-project_test"
useComposeFiles = listOf("docker-compose-test.yml") useComposeFiles = listOf("docker-compose-test.yml")
stopContainers = false stopContainers = false
isRequiredBy(project.tasks.test)
} }
} }
@@ -320,6 +306,74 @@ tasks.named("testComposeUp").configure {
} }
} }
tasks.register("testWithDependencies", Test::class) {
group = "tests"
dependsOn(tasks.named("testComposeUp"))
dependsOn(tasks.ktlintCheck)
dependsOn(testSql)
finalizedBy(tasks.sonarqube) // report is always generated after tests run
}
tasks.register("testArticles", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("article")
}
}
tasks.register("testCitizens", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("citizen")
}
}
tasks.register("testComments", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("comment")
}
}
tasks.register("testConstitutions", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("constitution")
}
}
tasks.register("testFollows", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("follow")
}
}
tasks.register("testNotifications", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("notification")
}
}
tasks.register("testOpinions", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("opinion")
}
}
tasks.register("testVotes", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("vote")
}
}
tasks.register("testWorkgroups", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("workgroup")
}
}
tasks.register("testViews", Test::class) {
group = "tests"
useJUnitPlatform {
includeTags("view")
}
}
dependencyCheck { dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML) formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
} }
@@ -327,8 +381,9 @@ dependencyCheck {
repositories { repositories {
mavenLocal() mavenLocal()
jcenter() jcenter()
maven { url = uri("https://kotlin.bintray.com/ktor") } maven("https://kotlin.bintray.com/ktor")
maven { url = uri("https://jitpack.io") } maven("https://jitpack.io")
maven("https://dl.bintray.com/konform-kt/konform")
} }
dependencies { dependencies {
@@ -359,6 +414,7 @@ dependencies {
implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1") implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
implementation("com.jayway.jsonpath:json-path:2.5.0") implementation("com.jayway.jsonpath:json-path:2.5.0")
implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.0") implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.0")
implementation("io.konform:konform-jvm:0.2.0")
testImplementation("io.ktor:ktor-server-tests:$ktorVersion") testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock:$ktorVersion") testImplementation("io.ktor:ktor-client-mock:$ktorVersion")

View File

@@ -1,6 +1,10 @@
package fr.dcproject.application package fr.dcproject.application
import fr.dcproject.application.http.BadRequestException
import fr.dcproject.application.http.HttpErrorBadRequest
import fr.dcproject.application.http.HttpErrorBadRequest.InvalidParam
import io.ktor.features.DataConversion import io.ktor.features.DataConversion
import io.ktor.http.HttpStatusCode
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
import org.koin.core.parameter.ParametersDefinition import org.koin.core.parameter.ParametersDefinition
@@ -8,6 +12,7 @@ import org.koin.core.qualifier.Qualifier
import java.util.UUID import java.util.UUID
private typealias ConverterDeclaration = DataConversion.Configuration.() -> Unit private typealias ConverterDeclaration = DataConversion.Configuration.() -> Unit
private inline fun <reified T> DataConversion.Configuration.get( private inline fun <reified T> DataConversion.Configuration.get(
qualifier: Qualifier? = null, qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null noinline parameters: ParametersDefinition? = null
@@ -17,7 +22,21 @@ private inline fun <reified T> DataConversion.Configuration.get(
val converters: ConverterDeclaration = { val converters: ConverterDeclaration = {
convert<UUID> { convert<UUID> {
decode { values, _ -> decode { values, _ ->
try {
values.singleOrNull()?.let { UUID.fromString(it) } values.singleOrNull()?.let { UUID.fromString(it) }
} catch (e: Throwable) {
throw BadRequestException(
HttpErrorBadRequest(
HttpStatusCode.BadRequest,
invalidParams = listOf(
InvalidParam(
"ID",
"must be UUID"
)
)
)
)
}
} }
encode { value -> encode { value ->

View File

@@ -0,0 +1,35 @@
package fr.dcproject.application.http
import fr.dcproject.application.http.HttpErrorBadRequest.InvalidParam
import io.konform.validation.ValidationResult
import io.ktor.http.HttpStatusCode
class BadRequestException(val httpError: HttpErrorBadRequest) : Exception()
class HttpErrorBadRequest(
statusCode: HttpStatusCode,
val title: String = statusCode.description,
val invalidParams: List<InvalidParam>,
) {
val statusCode: Int = statusCode.value
data class InvalidParam(
val name: String,
val reason: String
)
}
fun ValidationResult<*>.toOutput() = HttpErrorBadRequest(
HttpStatusCode.BadRequest,
invalidParams = this.errors.map {
InvalidParam(
it.dataPath,
it.message
)
}
)
fun ValidationResult<*>.badRequestIfNotValid() {
if (errors.size > 0) {
throw BadRequestException(toOutput())
}
}

View File

@@ -6,6 +6,7 @@ import fr.dcproject.component.auth.ForbiddenException
import fr.dcproject.component.auth.user import fr.dcproject.component.auth.user
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.features.ParameterConversionException
import io.ktor.features.StatusPages import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.response.respond import io.ktor.response.respond
@@ -13,18 +14,10 @@ import java.util.concurrent.CompletionException
class HttpError( class HttpError(
statusCode: HttpStatusCode, statusCode: HttpStatusCode,
val cause: Throwable? = null, cause: Throwable? = null,
val type: String? = null,
val title: String = cause?.message ?: statusCode.description, val title: String = cause?.message ?: statusCode.description,
val detail: String? = null,
val invalidParams: List<InvalidParam>? = null,
val stackTrace: String? = cause?.stackTraceToString()
) { ) {
val statusCode: Int = statusCode.value val statusCode: Int = statusCode.value
data class InvalidParam(
val name: String,
val reason: String
)
} }
fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = { fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = {
@@ -79,4 +72,15 @@ fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = {
call.respond(HttpStatusCode.Forbidden, it) call.respond(HttpStatusCode.Forbidden, it)
} }
} }
exception<BadRequestException> { e ->
call.respond(HttpStatusCode.BadRequest, e.httpError)
}
exception<ParameterConversionException> { e ->
val parent = e.cause
if (parent is BadRequestException) {
call.respond(HttpStatusCode.BadRequest, parent.httpError)
} else {
throw e
}
}
} }

View File

@@ -6,9 +6,6 @@ interface PaginatedRequestI {
} }
open class PaginatedRequest( open class PaginatedRequest(
page: Int = 1, override val page: Int = 1,
limit: Int = 50 override val limit: Int = 50
) : PaginatedRequestI { ) : PaginatedRequestI
override val page: Int = if (page < 1) 1 else page
override val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}

View File

@@ -4,7 +4,6 @@ import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.PathNotFoundException import com.jayway.jsonpath.PathNotFoundException
import org.apache.http.util.EntityUtils import org.apache.http.util.EntityUtils
import org.elasticsearch.client.Response import org.elasticsearch.client.Response
import org.slf4j.LoggerFactory
fun Response.contentToString(): String { fun Response.contentToString(): String {
return EntityUtils.toString(this.entity) return EntityUtils.toString(this.entity)
@@ -22,8 +21,6 @@ fun String.getJsonField(jsonPath: String): Int? {
return try { return try {
JsonPath.read(this, jsonPath) JsonPath.read(this, jsonPath)
} catch (e: PathNotFoundException) { } catch (e: PathNotFoundException) {
LoggerFactory.getLogger("fr.dcproject.utils.getJsonField")
.warn("No value for Json path ${JsonPath.compile(jsonPath).path}")
null null
} }
} }

View File

@@ -0,0 +1,41 @@
package fr.dcproject.common.utils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
@ExperimentalTime
fun <T> retry(numOfRetries: Int, duration: Duration = Duration.ZERO, block: (RetryContext) -> T): T {
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.utils.retry")
var throwable: Throwable? = null
for (attempt in 1..numOfRetries) {
val context = RetryContext()
try {
val output = block(context)
if (context.hasStop()) {
break
}
return output
} catch (e: Throwable) {
throwable = e
logger.debug("Failed attempt $attempt / $numOfRetries. Wait ${duration.inSeconds} seconds")
Thread.sleep(duration.inMilliseconds.toLong())
} finally {
if (context.hasStop()) {
break
}
}
}
throw throwable!!
}
class RetryContext() {
var stoped = false
fun stop() {
stoped = true
}
fun hasStop(): Boolean = stoped
}

View File

@@ -0,0 +1,6 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
import io.konform.validation.jsonschema.pattern
fun ValidationBuilder<String>.email() = pattern(""".+@.+\..+""")

View File

@@ -0,0 +1,22 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
fun ValidationBuilder<String>.passwordScore(minScore: Int) =
addConstraint("is not enough strong. Use Upper case, Lower case and special characters or juste use more characters.") { value ->
value.passwordScore() >= minScore
}
fun String.passwordScore(): Int {
var score: Int = length
val alphaNum = ('a'..'z').toList() + ('A'..'Z').toList() + ('0'..'9').toList()
val specialCount = length - toList().intersect(alphaNum).size
score += specialCount.let { if (it > 3) 3 else it }
val hasAlphaLower = toList().intersect(('a'..'z').toList()).size.let { if (it > 2) 2 else it }
val hasAlphaUpper = toList().intersect(('A'..'Z').toList()).size.let { if (it > 2) 2 else it }
val hasNum = toList().intersect(('0'..'9').toList()).size.let { if (it > 2) 2 else it }
score += (hasAlphaLower + hasAlphaUpper + hasNum - 2) * 2
return score
}

View File

@@ -0,0 +1,15 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
import java.net.MalformedURLException
import java.net.URL
fun ValidationBuilder<String>.isUrl() =
addConstraint("is not url") {
try {
URL(it)
true
} catch (e: MalformedURLException) {
false
}
}

View File

@@ -0,0 +1,14 @@
package fr.dcproject.common.validation
import io.konform.validation.ValidationBuilder
import java.util.UUID
fun ValidationBuilder<String>.isUuid() =
addConstraint("must be UUID") {
try {
UUID.fromString(it)
true
} catch (exception: IllegalArgumentException) {
false
}
}

View File

@@ -1,42 +1,69 @@
package fr.dcproject.component.article.routes package fr.dcproject.component.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.toUUID
import fr.dcproject.common.validation.isUuid
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.ArticleForListing
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.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
import io.ktor.locations.get import io.ktor.locations.get
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object FindArticleVersions { object FindArticleVersions {
@Location("/articles/{article}/versions") @Location("/articles/{article}/versions")
class ArticleVersionsRequest( class ArticleVersionsRequest(
article: UUID, val article: String,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val sort: String? = null, val sort: String? = null,
val direction: RepositoryI.Direction? = null, val direction: RepositoryI.Direction? = null,
val search: String? = null val search: String? = null
) { ) : PaginatedRequestI by PaginatedRequest(page, limit) {
val page: Int = if (page < 1) 1 else page fun validate() = Validation<ArticleVersionsRequest> {
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit ArticleVersionsRequest::page {
val article = ArticleRef(article) minimum(1)
maximum(100)
}
ArticleVersionsRequest::limit {
minimum(1)
maximum(50)
}
ArticleVersionsRequest::sort ifPresent {
enum(
"title",
"createdAt",
"vote",
"popularity",
)
}
ArticleVersionsRequest::article {
isUuid()
}
}.validate(this)
} }
private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) = private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) =
findVersionsById(request.page, request.limit, request.article.id) findVersionsById(request.page, request.limit, request.article.toUUID())
fun Route.findArticleVersions(repo: ArticleRepository, ac: ArticleAccessControl) { fun Route.findArticleVersions(repo: ArticleRepository, ac: ArticleAccessControl) {
get<ArticleVersionsRequest> { get<ArticleVersionsRequest> {
it.validate().badRequestIfNotValid()
repo.findVersions(it) repo.findVersions(it)
.apply { ac.assert { canView(result, citizenOrNull) } } .apply { ac.assert { canView(result, citizenOrNull) } }
.run { .run {

View File

@@ -1,7 +1,9 @@
package fr.dcproject.component.article.routes package fr.dcproject.component.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.validation.isUuid
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.ArticleForListing
import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.article.database.ArticleRepository
@@ -10,6 +12,10 @@ import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
@@ -28,7 +34,31 @@ object FindArticles {
val search: String? = null, val search: String? = null,
val createdBy: String? = null, val createdBy: String? = null,
val workgroup: String? = null val workgroup: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) ) : PaginatedRequestI by PaginatedRequest(page, limit) {
fun validate() = Validation<ArticlesRequest> {
ArticlesRequest::page {
minimum(1)
}
ArticlesRequest::limit {
minimum(1)
maximum(50)
}
ArticlesRequest::sort ifPresent {
enum(
"title",
"createdAt",
"vote",
"popularity",
)
}
ArticlesRequest::createdBy ifPresent {
isUuid()
}
ArticlesRequest::workgroup ifPresent {
isUuid()
}
}.validate(this)
}
private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<ArticleForListing> { private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<ArticleForListing> {
return find( return find(
@@ -43,6 +73,8 @@ object FindArticles {
fun Route.findArticles(repo: ArticleRepository, ac: ArticleAccessControl) { fun Route.findArticles(repo: ArticleRepository, ac: ArticleAccessControl) {
get<ArticlesRequest> { get<ArticlesRequest> {
it.validate().badRequestIfNotValid()
repo.findArticles(it) repo.findArticles(it)
.apply { ac.assert { canView(result, citizenOrNull) } } .apply { ac.assert { canView(result, citizenOrNull) } }
.let { .let {

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.article.routes package fr.dcproject.component.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert 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
@@ -12,6 +13,11 @@ import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.notification.ArticleUpdateNotification import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.Publisher import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.workgroup.database.WorkgroupRef import fr.dcproject.component.workgroup.database.WorkgroupRef
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxItems
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minItems
import io.konform.validation.jsonschema.minLength
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.application.call import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -35,11 +41,31 @@ object UpsertArticle {
val draft: Boolean = false, val draft: Boolean = false,
val versionId: UUID, val versionId: UUID,
val workgroup: WorkgroupRef? = null, val workgroup: WorkgroupRef? = null,
) ) {
fun validate() = Validation<Input> {
Input::title {
minLength(5)
maxLength(80)
}
Input::content {
minLength(50)
maxLength(6000)
}
Input::description {
minLength(50)
maxLength(6000)
}
Input::tags {
minItems(0)
maxItems(15)
}
}.validate(this)
}
} }
fun Route.upsertArticle(repo: ArticleRepository, publisher: Publisher, ac: ArticleAccessControl) { fun Route.upsertArticle(repo: ArticleRepository, publisher: Publisher, ac: ArticleAccessControl) {
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest<Input>().run { suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest<Input>().run {
validate().badRequestIfNotValid()
ArticleForUpdate( ArticleForUpdate(
id = id ?: UUID.randomUUID(), id = id ?: UUID.randomUUID(),
title = title, title = title,

View File

@@ -1,7 +1,10 @@
package fr.dcproject.component.auth.routes package fr.dcproject.component.auth.routes
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.common.validation.email
import fr.dcproject.common.validation.passwordScore
import fr.dcproject.component.auth.database.UserForCreate import fr.dcproject.component.auth.database.UserForCreate
import fr.dcproject.component.auth.database.UserI import fr.dcproject.component.auth.database.UserI
import fr.dcproject.component.auth.jwt.makeToken import fr.dcproject.component.auth.jwt.makeToken
@@ -9,6 +12,9 @@ import fr.dcproject.component.auth.routes.Register.RegisterRequest.Input
import fr.dcproject.component.citizen.database.CitizenForCreate import fr.dcproject.component.citizen.database.CitizenForCreate
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRepository import fr.dcproject.component.citizen.database.CitizenRepository
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.BadRequestException import io.ktor.features.BadRequestException
import io.ktor.http.ContentType import io.ktor.http.ContentType
@@ -43,6 +49,35 @@ object Register {
val username: String, val username: String,
val password: String val password: String
) )
fun validate() = Validation<Input> {
Input::name {
Name::firstName {
minLength(2)
maxLength(50)
}
Name::lastName {
minLength(2)
maxLength(50)
}
Name::civility ifPresent {
minLength(1)
maxLength(10)
}
}
Input::user {
User::username {
minLength(7)
maxLength(30)
}
User::password {
passwordScore(15)
}
}
Input::email {
email()
}
}.validate(this)
} }
} }
@@ -62,7 +97,10 @@ object Register {
post<RegisterRequest> { post<RegisterRequest> {
try { try {
val citizen = call.receiveOrBadRequest<Input>().toCitizen() val citizen = call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
.toCitizen()
citizenRepo.insertWithUser(citizen)?.user?.makeToken()?.let { token -> citizenRepo.insertWithUser(citizen)?.user?.makeToken()?.let { token ->
if (call.request.accept() == ContentType.Application.Json.toString()) { if (call.request.accept() == ContentType.Application.Json.toString()) {
call.respond( call.respond(

View File

@@ -1,7 +1,9 @@
package fr.dcproject.component.citizen.routes package fr.dcproject.component.citizen.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.common.validation.passwordScore
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.database.UserRepository import fr.dcproject.component.auth.database.UserRepository
@@ -9,6 +11,7 @@ import fr.dcproject.component.auth.database.UserWithPassword
import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.CitizenAccessControl import fr.dcproject.component.citizen.CitizenAccessControl
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import io.konform.validation.Validation
import io.ktor.application.call import io.ktor.application.call
import io.ktor.auth.UserPasswordCredential import io.ktor.auth.UserPasswordCredential
import io.ktor.features.BadRequestException import io.ktor.features.BadRequestException
@@ -25,14 +28,21 @@ object ChangeMyPassword {
@Location("/citizens/{citizen}/password/change") @Location("/citizens/{citizen}/password/change")
class ChangePasswordCitizenRequest(citizen: UUID) { class ChangePasswordCitizenRequest(citizen: UUID) {
val citizen = CitizenRef(citizen) val citizen = CitizenRef(citizen)
data class Input(val oldPassword: String, val newPassword: String) data class Input(val oldPassword: String, val newPassword: String) {
fun validate() = Validation<Input> {
Input::newPassword {
passwordScore(15)
}
}.validate(this)
}
} }
fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) { fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) {
put<ChangePasswordCitizenRequest> { put<ChangePasswordCitizenRequest> {
mustBeAuth() mustBeAuth()
ac.assert { canChangePassword(it.citizen, citizenOrNull) }
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>() val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
.apply { validate().badRequestIfNotValid() }
ac.assert { canChangePassword(it.citizen, citizenOrNull) }
userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password") userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password")
userRepository.changePassword( userRepository.changePassword(
UserWithPassword( UserWithPassword(

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.citizen.routes package fr.dcproject.component.citizen.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
@@ -10,6 +11,10 @@ import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.routes.PaginatedRequest import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
@@ -27,11 +32,28 @@ object FindCitizens {
val sort: String? = null, val sort: String? = null,
val direction: RepositoryI.Direction? = null, val direction: RepositoryI.Direction? = null,
val search: String? = null val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) ) : PaginatedRequestI by PaginatedRequest(page, limit) {
fun validate() = Validation<CitizensRequest> {
CitizensRequest::page {
minimum(1)
}
CitizensRequest::limit {
minimum(1)
maximum(50)
}
CitizensRequest::sort ifPresent {
enum(
"title",
"createdAt",
)
}
}.validate(this)
}
fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) { fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) {
get<CitizensRequest> { get<CitizensRequest> {
mustBeAuth() mustBeAuth()
it.validate().badRequestIfNotValid()
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
ac.assert { canView(citizens.result, citizenOrNull) } ac.assert { canView(citizens.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -41,7 +41,7 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
target: EntityI, target: EntityI,
page: Int, page: Int,
limit: Int, limit: Int,
sort: Sort sort: String
): Paginated<CommentForView<ArticleForView, CitizenCreatorI>> { ): Paginated<CommentForView<ArticleForView, CitizenCreatorI>> {
return requester return requester
.getFunction("find_comments_by_target") .getFunction("find_comments_by_target")
@@ -49,18 +49,7 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
page, page,
limit, limit,
"target_id" to target.id, "target_id" to target.id,
"sort" to sort.sql "sort" to sort
) as Paginated<CommentForView<ArticleForView, CitizenCreatorI>> ) as Paginated<CommentForView<ArticleForView, CitizenCreatorI>>
} }
enum class Sort(val sql: String) {
CREATED_AT("created_at"),
VOTES("votes");
companion object {
fun fromString(string: String): Sort? {
return values().firstOrNull { it.sql == string }
}
}
}
} }

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.comment.article.routes package fr.dcproject.component.comment.article.routes
import fr.dcproject.common.response.toOutput import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
@@ -12,6 +12,9 @@ import fr.dcproject.component.comment.article.routes.CreateCommentArticle.PostAr
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
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -26,13 +29,22 @@ object CreateCommentArticle {
@Location("/articles/{article}/comments") @Location("/articles/{article}/comments")
class PostArticleCommentRequest(article: UUID) { class PostArticleCommentRequest(article: UUID) {
val article = ArticleRef(article) val article = ArticleRef(article)
class Input(val content: String) class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
} }
fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) { fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) {
post<PostArticleCommentRequest> { post<PostArticleCommentRequest> {
mustBeAuth() mustBeAuth()
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
.run {
CommentForUpdate( CommentForUpdate(
target = it.article, target = it.article,
createdBy = citizen, createdBy = citizen,

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.comment.article.routes package fr.dcproject.component.comment.article.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
@@ -9,6 +10,10 @@ import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import fr.dcproject.routes.PaginatedRequest import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI import fr.dcproject.routes.PaginatedRequestI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -26,14 +31,31 @@ object GetArticleComments {
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val search: String? = null, val search: String? = null,
sort: String = CommentArticleRepository.Sort.CREATED_AT.sql val sort: String = "createdAt"
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) : PaginatedRequestI by PaginatedRequest(page, limit) {
val article = ArticleRef(article) val article = ArticleRef(article)
val sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.fromString(sort) ?: CommentArticleRepository.Sort.CREATED_AT
fun validate() = Validation<ArticleCommentsRequest> {
ArticleCommentsRequest::page {
minimum(1)
}
ArticleCommentsRequest::limit {
minimum(1)
maximum(50)
}
ArticleCommentsRequest::sort ifPresent {
enum(
"votes",
"createdAt",
)
}
}.validate(this)
} }
fun Route.getArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) { fun Route.getArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) {
get<ArticleCommentsRequest> { get<ArticleCommentsRequest> {
it.validate().badRequestIfNotValid()
val comments = repo.findByTarget(it.article, it.page, it.limit, it.sort) val comments = repo.findByTarget(it.article, it.page, it.limit, it.sort)
if (comments.result.isNotEmpty()) { if (comments.result.isNotEmpty()) {
ac.assert { canView(comments.result, citizenOrNull) } ac.assert { canView(comments.result, citizenOrNull) }

View File

@@ -5,7 +5,6 @@ import fr.dcproject.common.entity.TargetI
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenCreatorI import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.comment.article.database.CommentArticleRepository
import fr.dcproject.component.comment.generic.database.CommentForView import fr.dcproject.component.comment.generic.database.CommentForView
import fr.dcproject.component.comment.generic.database.CommentRepositoryAbs import fr.dcproject.component.comment.generic.database.CommentRepositoryAbs
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
@@ -41,7 +40,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
target: EntityI, target: EntityI,
page: Int, page: Int,
limit: Int, limit: Int,
sort: CommentArticleRepository.Sort sort: String
): Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> { ): Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> {
return requester.run { return requester.run {
getFunction("find_comments_by_target") getFunction("find_comments_by_target")
@@ -49,7 +48,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
page, page,
limit, limit,
"target_id" to target.id, "target_id" to target.id,
"sort" to sort.sql "sort" to sort
) )
as Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> as Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>>
} }

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.comment.constitution.routes package fr.dcproject.component.comment.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
@@ -12,6 +13,9 @@ import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -26,13 +30,23 @@ object CreateConstitutionComment {
@Location("/constitutions/{constitution}/comments") @Location("/constitutions/{constitution}/comments")
class CreateConstitutionCommentRequest(constitution: UUID) { class CreateConstitutionCommentRequest(constitution: UUID) {
val constitution = ConstitutionRef(constitution) val constitution = ConstitutionRef(constitution)
class Input(val content: String) class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
} }
fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) { fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
post<CreateConstitutionCommentRequest> { post<CreateConstitutionCommentRequest> {
mustBeAuth() mustBeAuth()
call.receiveOrBadRequest<Input>().run {
call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
.run {
CommentForUpdate( CommentForUpdate(
target = it.constitution, target = it.constitution,
createdBy = citizen, createdBy = citizen,

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.comment.constitution.routes package fr.dcproject.component.comment.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
@@ -7,6 +8,12 @@ import fr.dcproject.component.comment.constitution.database.CommentConstitutionR
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -19,12 +26,36 @@ import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object GetConstitutionComment { object GetConstitutionComment {
@Location("/constitutions/{constitution}/comments") @Location("/constitutions/{constitution}/comments")
class GetConstitutionCommentRequest(constitution: UUID) { class GetConstitutionCommentRequest(
constitution: UUID,
page: Int = 1,
limit: Int = 50,
val search: String? = null,
val sort: String = "createdAt"
) : PaginatedRequestI by PaginatedRequest(page, limit) {
val constitution = ConstitutionRef(constitution) val constitution = ConstitutionRef(constitution)
fun validate() = Validation<GetConstitutionCommentRequest> {
GetConstitutionCommentRequest::page {
minimum(1)
}
GetConstitutionCommentRequest::limit {
minimum(1)
maximum(50)
}
GetConstitutionCommentRequest::sort ifPresent {
enum(
"votes",
"createdAt",
)
}
}.validate(this)
} }
fun Route.getConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) { fun Route.getConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
get<GetConstitutionCommentRequest> { get<GetConstitutionCommentRequest> {
it.validate().badRequestIfNotValid()
val comments = repo.findByTarget(it.constitution) val comments = repo.findByTarget(it.constitution)
ac.assert { canView(comments.result, citizenOrNull) } ac.assert { canView(comments.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -63,12 +63,14 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
constructor( constructor(
createdBy: C, createdBy: C,
parent: CommentParent<T>, parent: CommentParent<T>,
content: String content: String,
id: UUID? = null,
) : this( ) : this(
createdBy = createdBy, createdBy = createdBy,
parent = parent, parent = parent,
target = parent.target, target = parent.target,
content = content content = content,
id = id ?: UUID.randomUUID(),
) )
} }

View File

@@ -6,7 +6,6 @@ import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenCreatorI import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.comment.article.database.CommentArticleRepository
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
@@ -22,7 +21,7 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
): Paginated<CommentForView<T, CitizenCreatorI>> ): Paginated<CommentForView<T, CitizenCreatorI>>
open fun findByParent( open fun findByParent(
parent: CommentForView<T, CitizenCreatorI>, parent: CommentI,
page: Int = 1, page: Int = 1,
limit: Int = 50 limit: Int = 50
): Paginated<CommentForView<T, CitizenCreatorI>> { ): Paginated<CommentForView<T, CitizenCreatorI>> {
@@ -49,7 +48,7 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
target: EntityI, target: EntityI,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT sort: String = "createdAt"
): Paginated<CommentForView<T, CitizenCreatorI>> { ): Paginated<CommentForView<T, CitizenCreatorI>> {
return findByTarget(target.id, page, limit, sort) return findByTarget(target.id, page, limit, sort)
} }
@@ -58,36 +57,30 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
targetId: UUID, targetId: UUID,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT sort: String = "createdAt"
): Paginated<CommentForView<T, CitizenCreatorI>> { ): Paginated<CommentForView<T, CitizenCreatorI>> = requester
return requester.run { .getFunction("find_comments_by_target")
getFunction("find_comments_by_target")
.select<CommentForView<T, CitizenCreator>>( .select<CommentForView<T, CitizenCreator>>(
page, page,
limit, limit,
"target_id" to targetId, "target_id" to targetId,
"sort" to sort.sql "sort" to sort
) ) as Paginated<CommentForView<T, CitizenCreatorI>>
as Paginated<CommentForView<T, CitizenCreatorI>>
}
}
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>) { fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>): CommentForView<TargetRef, CitizenCreator> = requester
requester
.getFunction("comment") .getFunction("comment")
.sendQuery( .selectOne(
"reference" to comment.target.reference, "reference" to comment.target.reference,
"resource" to comment "resource" to comment
) )!!
}
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>) { fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>): CommentForView<TargetRef, CitizenCreator> {
requester return requester
.getFunction("edit_comment") .getFunction("edit_comment")
.sendQuery( .selectOne(
"id" to comment.id, "id" to comment.id,
"content" to comment.content "content" to comment.content
) )!!
} }
} }

View File

@@ -0,0 +1,63 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object CreateComment {
@Location("/comments/{comment}")
class CreateCommentRequest(comment: UUID) {
val comment = CommentRef(comment)
class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
}
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
post<CreateCommentRequest> {
mustBeAuth()
call.receiveOrBadRequest<CreateCommentRequest.Input>()
.apply { validate().badRequestIfNotValid() }
.run {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
CommentForUpdate(
content = content,
createdBy = citizen,
target = parent.target,
parent = parent,
)
}.let { newComment ->
ac.assert { canCreate(newComment, citizenOrNull) }
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment.toOutput())
}
}
}
}

View File

@@ -1,47 +0,0 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput
import io.ktor.application.call
import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object CreateCommentChildren {
@Location("/comments/{comment}/children")
class CreateCommentChildrenRequest(comment: UUID) {
val comment = CommentRef(comment)
class Input(val content: String)
}
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
post<CreateCommentChildrenRequest> {
mustBeAuth()
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = CommentForUpdate(
content = call.receiveOrBadRequest<CreateCommentChildrenRequest.Input>().content,
createdBy = citizen,
parent = parent
)
ac.assert { canCreate(newComment, citizenOrNull) }
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment.toOutput())
}
}
}

View File

@@ -1,14 +1,18 @@
package fr.dcproject.component.comment.generic.routes package fr.dcproject.component.comment.generic.routes
import fr.dcproject.common.response.toOutput import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.auth.mustBeAuth
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.CommentRef import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -24,22 +28,40 @@ object EditComment {
@Location("/comments/{comment}") @Location("/comments/{comment}")
class EditCommentRequest(comment: UUID) { class EditCommentRequest(comment: UUID) {
val comment = CommentRef(comment) val comment = CommentRef(comment)
class Input(val content: String) class Input(val content: String) {
fun validate() = Validation<Input> {
Input::content {
minLength(20)
maxLength(6000)
}
}.validate(this)
}
} }
fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) { fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) {
put<EditCommentRequest> { put<EditCommentRequest> {
mustBeAuth() mustBeAuth()
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.assert { canUpdate(comment, citizenOrNull) } ac.assert { canUpdate(commentOld, citizenOrNull) }
comment.content = call.receiveOrBadRequest<EditCommentRequest.Input>().content
repo.edit(comment)
call.receiveOrBadRequest<EditCommentRequest.Input>()
.apply { validate().badRequestIfNotValid() }
.run {
CommentForUpdate(
id = commentOld.id,
createdBy = commentOld.createdBy,
target = commentOld.target,
parent = commentOld.parent,
content = content,
)
}
.let { repo.edit(it) }
.let {
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,
comment.toOutput() it.toOutput()
) )
} }
} }
}
} }

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.comment.toOutput import fr.dcproject.component.comment.toOutput
import fr.dcproject.routes.PaginatedRequest import fr.dcproject.routes.PaginatedRequest
@@ -21,11 +22,13 @@ import java.util.UUID
object GetCommentChildren { object GetCommentChildren {
@Location("/comments/{comment}/children") @Location("/comments/{comment}/children")
class CommentChildrenRequest( class CommentChildrenRequest(
val comment: UUID, comment: UUID,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val search: String? = null val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) ) : PaginatedRequestI by PaginatedRequest(page, limit) {
val comment = CommentRef(comment)
}
fun Route.getChildrenComments(repo: CommentRepository, ac: CommentAccessControl) { fun Route.getChildrenComments(repo: CommentRepository, ac: CommentAccessControl) {
get<CommentChildrenRequest> { get<CommentChildrenRequest> {

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.comment.generic.routes package fr.dcproject.component.comment.generic.routes
import fr.dcproject.component.comment.generic.routes.CreateCommentChildren.createCommentChildren import fr.dcproject.component.comment.generic.routes.CreateComment.createCommentChildren
import fr.dcproject.component.comment.generic.routes.EditComment.editComment import fr.dcproject.component.comment.generic.routes.EditComment.editComment
import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments
import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.constitution.routes package fr.dcproject.component.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
@@ -15,6 +16,9 @@ import fr.dcproject.component.constitution.database.ConstitutionForUpdate.TitleF
import fr.dcproject.component.constitution.database.ConstitutionRepository import fr.dcproject.component.constitution.database.ConstitutionRepository
import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input
import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input.Title import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input.Title
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -36,7 +40,6 @@ object CreateConstitution {
val draft: Boolean = false, val draft: Boolean = false,
val versionId: UUID = UUID.randomUUID() val versionId: UUID = UUID.randomUUID()
) { ) {
class Title( class Title(
val id: UUID = UUID.randomUUID(), val id: UUID = UUID.randomUUID(),
val name: String, val name: String,
@@ -44,10 +47,25 @@ object CreateConstitution {
) { ) {
class ArticleRef(val id: UUID) class ArticleRef(val id: UUID)
} }
fun validate() = Validation<Input> {
Input::title {
minLength(10)
maxLength(80)
}
Input::titles onEach {
Title::name {
minLength(10)
maxLength(80)
}
}
}.validate(this)
} }
} }
private fun getNewConstitution(input: Input, citizen: Citizen) = input.run { private fun getNewConstitution(input: Input, citizen: Citizen) = input.run {
validate().badRequestIfNotValid()
ConstitutionForUpdate<CitizenWithUserI, TitleForUpdate<ArticleRef>>( ConstitutionForUpdate<CitizenWithUserI, TitleForUpdate<ArticleRef>>(
id = UUID.randomUUID(), id = UUID.randomUUID(),
title = title, title = title,

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.constitution.routes package fr.dcproject.component.constitution.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
@@ -8,6 +9,10 @@ import fr.dcproject.component.constitution.database.ConstitutionRepository
import fr.dcproject.routes.PaginatedRequest import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -27,10 +32,27 @@ object FindConstitutions {
val sort: String? = null, val sort: String? = null,
val direction: RepositoryI.Direction? = null, val direction: RepositoryI.Direction? = null,
val search: String? = null val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) ) : PaginatedRequestI by PaginatedRequest(page, limit) {
fun validate() = Validation<FindConstitutionsRequest> {
FindConstitutionsRequest::page {
minimum(1)
}
FindConstitutionsRequest::limit {
minimum(1)
maximum(50)
}
FindConstitutionsRequest::sort ifPresent {
enum(
"title",
"createdAt",
)
}
}.validate(this)
}
fun Route.findConstitutions(repo: ConstitutionRepository, ac: ConstitutionAccessControl) { fun Route.findConstitutions(repo: ConstitutionRepository, ac: ConstitutionAccessControl) {
get<FindConstitutionsRequest> { get<FindConstitutionsRequest> {
it.validate().badRequestIfNotValid()
val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search) val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
ac.assert { canView(constitutions.result, citizenOrNull) } ac.assert { canView(constitutions.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -1,5 +1,7 @@
package fr.dcproject.component.notification package fr.dcproject.component.notification
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
@@ -12,6 +14,10 @@ import fr.dcproject.component.article.database.ArticleForView
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
@JsonSubTypes(
JsonSubTypes.Type(value = ArticleUpdateNotification::class, name = "article")
)
open class Notification( open class Notification(
val type: String, val type: String,
val createdAt: DateTime = DateTime.now() val createdAt: DateTime = DateTime.now()

View File

@@ -28,12 +28,12 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
class NotificationsPush private constructor( class NotificationsPush(
private val redis: RedisAsyncCommands<String, String>, private val redis: RedisAsyncCommands<String, String>,
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>, private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
citizen: CitizenI, citizen: CitizenI,
incoming: Flow<Notification>, incoming: Flow<Notification>,
onRecieve: suspend (Notification) -> Unit, onReceive: suspend (Notification) -> Unit,
) { ) {
class Builder(val redisClient: RedisClient) { class Builder(val redisClient: RedisClient) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis") private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
@@ -43,8 +43,8 @@ class NotificationsPush private constructor(
fun build( fun build(
citizen: CitizenI, citizen: CitizenI,
incoming: Flow<Notification>, incoming: Flow<Notification>,
onRecieve: suspend (Notification) -> Unit, onReceive: suspend (Notification) -> Unit,
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve) ): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onReceive)
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
fun build(ws: DefaultWebSocketServerSession): NotificationsPush { fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
@@ -69,7 +69,7 @@ class NotificationsPush private constructor(
override fun message(pattern: String?, channel: String?, message: String?) { override fun message(pattern: String?, channel: String?, message: String?) {
runBlocking { runBlocking {
getNotifications().collect { getNotifications().collect {
onRecieve(it) onReceive(it)
} }
} }
} }
@@ -85,10 +85,12 @@ class NotificationsPush private constructor(
/* Get old notification and sent it to websocket */ /* Get old notification and sent it to websocket */
runBlocking { runBlocking {
getNotifications().collect { onRecieve(it) } getNotifications().collect {
onReceive(it)
}
} }
/* Lisen redis event, and sent the new notification into websocket */ /* Listen redis event, and sent the new notification into websocket */
redisConnectionPubSub.run { redisConnectionPubSub.run {
addListener(listener) addListener(listener)

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.opinion.routes package fr.dcproject.component.opinion.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.entity.TargetRef import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
@@ -12,6 +13,9 @@ import fr.dcproject.component.opinion.database.Opinion
import fr.dcproject.routes.PaginatedRequest import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -34,11 +38,22 @@ object GetMyOpinionsArticle {
limit: Int = 50 limit: Int = 50
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) : PaginatedRequestI by PaginatedRequest(page, limit) {
val citizen = CitizenRef(citizen) val citizen = CitizenRef(citizen)
fun validate() = Validation<CitizenOpinionsArticleRequest> {
CitizenOpinionsArticleRequest::page {
minimum(1)
}
CitizenOpinionsArticleRequest::limit {
minimum(1)
maximum(50)
}
}.validate(this)
} }
fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) { fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
get<CitizenOpinionsArticleRequest> { get<CitizenOpinionsArticleRequest> {
mustBeAuth() mustBeAuth()
it.validate().badRequestIfNotValid()
val opinions: Paginated<Opinion<TargetRef>> = repo.findCitizenOpinions(citizen, it.page, it.limit) val opinions: Paginated<Opinion<TargetRef>> = repo.findCitizenOpinions(citizen, it.page, it.limit)
ac.assert { canView(opinions.result, citizenOrNull) } ac.assert { canView(opinions.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.vote.routes package fr.dcproject.component.vote.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
@@ -9,6 +10,9 @@ import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteArticleRepository import fr.dcproject.component.vote.database.VoteArticleRepository
import fr.dcproject.routes.PaginatedRequest import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI import fr.dcproject.routes.PaginatedRequestI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -28,11 +32,22 @@ object GetCitizenVotesOnArticle {
val search: String? = null val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit) { ) : PaginatedRequestI by PaginatedRequest(page, limit) {
val citizen = CitizenRef(citizen) val citizen = CitizenRef(citizen)
fun validate() = Validation<CitizenVoteArticleRequest> {
CitizenVoteArticleRequest::page {
minimum(1)
}
CitizenVoteArticleRequest::limit {
minimum(1)
maximum(50)
}
}.validate(this)
} }
fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) { fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) {
get<CitizenVoteArticleRequest> { get<CitizenVoteArticleRequest> {
mustBeAuth() mustBeAuth()
it.validate().badRequestIfNotValid()
val votes = repo.findByCitizen(it.citizen, it.page, it.limit) val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
ac.assert { canView(votes.result, citizenOrNull) } ac.assert { canView(votes.result, citizenOrNull) }

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.vote.routes package fr.dcproject.component.vote.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
@@ -10,6 +11,9 @@ import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteArticleRepository import fr.dcproject.component.vote.database.VoteArticleRepository
import fr.dcproject.component.vote.database.VoteForUpdate import fr.dcproject.component.vote.database.VoteForUpdate
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -25,13 +29,22 @@ object PutVoteOnArticle {
@Location("/articles/{article}/vote") @Location("/articles/{article}/vote")
class ArticleVoteRequest(article: UUID) { class ArticleVoteRequest(article: UUID) {
val article = ArticleRef(article) val article = ArticleRef(article)
data class Input(var note: Int) data class Input(var note: Int) {
fun validate() = Validation<Input> {
Input::note {
minimum(-1)
maximum(1)
}
}.validate(this)
}
} }
fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) { fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) {
put<ArticleVoteRequest> { put<ArticleVoteRequest> {
mustBeAuth() mustBeAuth()
val input = call.receiveOrBadRequest<ArticleVoteRequest.Input>() val input = call.receiveOrBadRequest<ArticleVoteRequest.Input>()
.apply { validate().badRequestIfNotValid() }
val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found") val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
val vote = VoteForUpdate( val vote = VoteForUpdate(
target = article, target = article,

View File

@@ -1,15 +1,21 @@
package fr.dcproject.component.vote.routes package fr.dcproject.component.vote.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteCommentRepository import fr.dcproject.component.vote.database.VoteCommentRepository
import fr.dcproject.component.vote.database.VoteForUpdate import fr.dcproject.component.vote.database.VoteForUpdate
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.NotFoundException
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
@@ -21,18 +27,29 @@ import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object PutVoteOnComment { object PutVoteOnComment {
@Location("/comments/{comment}/vote") @Location("/comments/{comment}/vote")
class CommentVoteRequest(val comment: UUID) { class CommentVoteRequest(comment: UUID) {
data class Content(var note: Int) val comment = CommentRef(comment)
data class Input(var note: Int) {
fun validate() = Validation<Input> {
Input::note {
minimum(-1)
maximum(1)
}
}.validate(this)
}
} }
fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, ac: VoteAccessControl) { fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, ac: VoteAccessControl) {
put<CommentVoteRequest> { put<CommentVoteRequest> {
mustBeAuth() mustBeAuth()
val comment = commentRepo.findById(it.comment)!!
val content = call.receiveOrBadRequest<CommentVoteRequest.Content>() val comment = commentRepo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
val input = call.receiveOrBadRequest<CommentVoteRequest.Input>()
.apply { validate().badRequestIfNotValid() }
val vote = VoteForUpdate( val vote = VoteForUpdate(
target = comment, target = comment,
note = content.note, note = input.note,
createdBy = this.citizen createdBy = this.citizen
) )
ac.assert { canCreate(vote, citizenOrNull) } ac.assert { canCreate(vote, citizenOrNull) }

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.vote.routes package fr.dcproject.component.vote.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
@@ -11,6 +12,9 @@ import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteConstitutionRepository import fr.dcproject.component.vote.database.VoteConstitutionRepository
import fr.dcproject.component.vote.database.VoteForUpdate import fr.dcproject.component.vote.database.VoteForUpdate
import fr.dcproject.component.vote.routes.PutVoteOnConstitution.ConstitutionVoteRequest.Input import fr.dcproject.component.vote.routes.PutVoteOnConstitution.ConstitutionVoteRequest.Input
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.NotFoundException import io.ktor.features.NotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -26,17 +30,25 @@ object PutVoteOnConstitution {
@Location("/constitutions/{constitution}/vote") @Location("/constitutions/{constitution}/vote")
class ConstitutionVoteRequest(constitution: UUID) { class ConstitutionVoteRequest(constitution: UUID) {
val constitution = ConstitutionRef(constitution) val constitution = ConstitutionRef(constitution)
data class Input(var note: Int) data class Input(var note: Int) {
fun validate() = Validation<Input> {
Input::note {
minimum(-1)
maximum(1)
}
}.validate(this)
}
} }
fun Route.voteConstitution(repo: VoteConstitutionRepository, ac: VoteAccessControl, constitutionRepo: ConstitutionRepository) { fun Route.voteConstitution(repo: VoteConstitutionRepository, ac: VoteAccessControl, constitutionRepo: ConstitutionRepository) {
put<ConstitutionVoteRequest> { put<ConstitutionVoteRequest> {
mustBeAuth() mustBeAuth()
val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}") val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}")
val content = call.receiveOrBadRequest<Input>() val input = call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
val vote = VoteForUpdate( val vote = VoteForUpdate(
target = constitution, target = constitution,
note = content.note, note = input.note,
createdBy = this.citizen createdBy = this.citizen
) )
ac.assert { canCreate(vote, citizenOrNull) } ac.assert { canCreate(vote, citizenOrNull) }

View File

@@ -1,8 +1,9 @@
package fr.dcproject.component.workgroup.routes package fr.dcproject.component.workgroup.routes
import fr.dcproject.common.response.toOutput import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.common.validation.isUrl
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.auth.mustBeAuth
@@ -10,6 +11,9 @@ import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupForUpdate import fr.dcproject.component.workgroup.database.WorkgroupForUpdate
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
import fr.dcproject.component.workgroup.routes.CreateWorkgroup.PostWorkgroupRequest.Input import fr.dcproject.component.workgroup.routes.CreateWorkgroup.PostWorkgroupRequest.Input
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -29,13 +33,30 @@ object CreateWorkgroup {
val description: String, val description: String,
val logo: String?, val logo: String?,
val anonymous: Boolean? val anonymous: Boolean?
) ) {
fun validate() = Validation<Input> {
Input::name {
minLength(5)
maxLength(80)
}
Input::description {
minLength(50)
maxLength(6000)
}
Input::logo ifPresent {
isUrl()
maxLength(2048)
}
}.validate(this)
}
} }
fun Route.createWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.createWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
post<PostWorkgroupRequest> { post<PostWorkgroupRequest> {
mustBeAuth() mustBeAuth()
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>().run {
validate().badRequestIfNotValid()
WorkgroupForUpdate( WorkgroupForUpdate(
id ?: UUID.randomUUID(), id ?: UUID.randomUUID(),
name, name,

View File

@@ -1,13 +1,18 @@
package fr.dcproject.component.workgroup.routes package fr.dcproject.component.workgroup.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.common.validation.isUrl
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupForUpdate import fr.dcproject.component.workgroup.database.WorkgroupForUpdate
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
import fr.dcproject.component.workgroup.routes.EditWorkgroup.PutWorkgroupRequest.Input import fr.dcproject.component.workgroup.routes.EditWorkgroup.PutWorkgroupRequest.Input
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -27,7 +32,22 @@ object EditWorkgroup {
val description: String?, val description: String?,
val logo: String?, val logo: String?,
val anonymous: Boolean? val anonymous: Boolean?
) ) {
fun validate() = Validation<Input> {
Input::name ifPresent {
minLength(5)
maxLength(80)
}
Input::description ifPresent {
minLength(50)
maxLength(6000)
}
Input::logo ifPresent {
isUrl()
maxLength(2048)
}
}.validate(this)
}
} }
fun Route.editWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.editWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
@@ -35,6 +55,7 @@ object EditWorkgroup {
mustBeAuth() mustBeAuth()
repo.findById(it.workgroupId)?.let { old -> repo.findById(it.workgroupId)?.let { old ->
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>().run {
validate().badRequestIfNotValid()
WorkgroupForUpdate( WorkgroupForUpdate(
id = old.id, id = old.id,
name = name ?: old.name, name = name ?: old.name,

View File

@@ -1,12 +1,20 @@
package fr.dcproject.component.workgroup.routes package fr.dcproject.component.workgroup.routes
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.toUUID import fr.dcproject.common.utils.toUUID
import fr.dcproject.common.validation.isUuid
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
import io.konform.validation.jsonschema.maximum
import io.konform.validation.jsonschema.minimum
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -27,16 +35,33 @@ object GetWorkgroups {
val search: String? = null, val search: String? = null,
val createdBy: String? = null, val createdBy: String? = null,
members: List<String?>? = null members: List<String?>? = null
) { ) : PaginatedRequestI by PaginatedRequest(page, limit) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
val members: List<UUID>? = members?.toUUID() val members: List<UUID>? = members?.toUUID()
fun validate() = Validation<WorkgroupsRequest> {
WorkgroupsRequest::page {
minimum(1)
}
WorkgroupsRequest::limit {
minimum(1)
maximum(50)
}
WorkgroupsRequest::sort ifPresent {
enum(
"name",
"createdAt",
)
}
WorkgroupsRequest::createdBy ifPresent {
isUuid()
}
}.validate(this)
} }
fun Route.getWorkgroups(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.getWorkgroups(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
get<WorkgroupsRequest> { get<WorkgroupsRequest> {
val workgroups = it.validate().badRequestIfNotValid()
repo.find(
val workgroups = repo.find(
it.page, it.page,
it.limit, it.limit,
it.sort, it.sort,

View File

@@ -41,6 +41,12 @@ paths:
maxItems: 50 maxItems: 50
items: items:
$ref: '#/components/schemas/ArticleListingResponse' $ref: '#/components/schemas/ArticleListingResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [] - JWTAuth: []
@@ -65,16 +71,21 @@ paths:
Limit power of press Limit power of press
content: content:
type: string type: string
minLength: 50
maxLength: 6000
example: example:
Lorem upsum... Lorem upsum...
description: description:
type: string type: string
minLength: 50
maxLength: 6000
example: example:
I think is the bether choice I think is the bether choice
tags: tags:
type: array type: array
items: items:
type: string type: string
maxItems: 15
default: [ ] default: [ ]
example: [ power, press ] example: [ power, press ]
anonymous: anonymous:
@@ -106,6 +117,12 @@ paths:
format: uuid format: uuid
versionNumber: versionNumber:
type: integer type: integer
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
403: 403:
@@ -128,6 +145,12 @@ paths:
tags: tags:
- article - article
operationId: getArticle operationId: getArticle
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/sort'
- $ref: '#/components/parameters/direction'
- $ref: '#/components/parameters/search'
responses: responses:
200: 200:
description: The Article objects description: The Article objects
@@ -135,6 +158,19 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ArticleResponse' $ref: '#/components/schemas/ArticleResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
404:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/404'
/articles/{article}/versions: /articles/{article}/versions:
parameters: parameters:
- $ref: '#/components/parameters/article' - $ref: '#/components/parameters/article'
@@ -143,6 +179,12 @@ paths:
tags: tags:
- article - article
operationId: getArticleVersions operationId: getArticleVersions
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/sort'
- $ref: '#/components/parameters/direction'
- $ref: '#/components/parameters/search'
responses: responses:
200: 200:
description: The versions of Article description: The versions of Article
@@ -193,6 +235,12 @@ paths:
format: uuid format: uuid
name: name:
type: string type: string
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/login: /login:
post: post:
@@ -310,7 +358,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
description: sdf $ref: '#/components/schemas/400'
/auth/passwordless: /auth/passwordless:
post: post:
summary: Send a connexion link by email summary: Send a connexion link by email
@@ -354,7 +402,7 @@ paths:
parameters: parameters:
- $ref: '#/components/parameters/page' - $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit' - $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/sort' - $ref: '#/components/parameters/citizenSort'
- $ref: '#/components/parameters/direction' - $ref: '#/components/parameters/direction'
- $ref: '#/components/parameters/search' - $ref: '#/components/parameters/search'
responses: responses:
@@ -371,6 +419,12 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/CitizenListResponse' $ref: '#/components/schemas/CitizenListResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/citizens/current: /citizens/current:
@@ -443,6 +497,10 @@ paths:
description: Password changed description: Password changed
400: 400:
description: Bad request description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
404: 404:
@@ -464,13 +522,13 @@ paths:
in: query in: query
required: false required: false
example: example:
- created_at - createdAt
- votes - votes
schema: schema:
type: string type: string
default: created_at default: createdAt
enum: enum:
- created_at - createdAt
- votes - votes
responses: responses:
200: 200:
@@ -486,6 +544,12 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [ ] - JWTAuth: [ ]
@@ -503,8 +567,10 @@ paths:
properties: properties:
content: content:
type: string type: string
minLength: 20
maxLength: 6000
example: example:
Lorem ipsum... Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum...
responses: responses:
201: 201:
description: Return created Comment description: Return created Comment
@@ -512,6 +578,12 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/comments/{comment}: /comments/{comment}:
@@ -528,6 +600,42 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
post:
security:
- JWTAuth: []
summary: create comment
tags:
- comment
requestBody:
content:
application/json:
schema:
required:
- content
properties:
content:
type: string
minLength: 20
maxLength: 6000
example:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
responses:
201:
description: Return updated comment
content:
application/json:
schema:
$ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401:
$ref: '#/components/responses/401'
404:
description: No comment found
put: put:
security: security:
- JWTAuth: [] - JWTAuth: []
@@ -543,8 +651,10 @@ paths:
properties: properties:
content: content:
type: string type: string
minLength: 20
maxLength: 6000
example: example:
Lorem ipsum... Lorem ipsum dolor sit amet, consectetur adipiscing elit.
responses: responses:
200: 200:
description: Return updated comment description: Return updated comment
@@ -552,6 +662,12 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/comments/{comment}/children: /comments/{comment}/children:
@@ -637,13 +753,42 @@ paths:
tags: tags:
- comment - comment
- constitution - constitution
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/search'
- name: sort
in: query
required: false
example:
- createdAt
- votes
schema:
type: string
default: createdAt
enum:
- createdAt
- votes
responses: responses:
200: 200:
description: Return Comment and children description: Return Comment and children
content: content:
application/json: application/json:
schema: schema:
allOf:
- $ref: '#/components/schemas/Paginated'
- type: object
properties:
result:
type: array
items:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [] - JWTAuth: []
@@ -660,8 +805,10 @@ paths:
properties: properties:
content: content:
type: string type: string
minLength: 20
maxLength: 6000
example: example:
Lorem ipsum... Lorem ipsum dolor sit amet, consectetur adipiscing elit.
responses: responses:
201: 201:
description: Return created comment description: Return created comment
@@ -669,6 +816,12 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CommentResponse' $ref: '#/components/schemas/CommentResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
@@ -698,6 +851,12 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/ConstitutionListingResponse' $ref: '#/components/schemas/ConstitutionListingResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
security: security:
- JWTAuth: [ ] - JWTAuth: [ ]
@@ -722,7 +881,11 @@ paths:
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
400: 400:
$ref: '#/components/responses/400' description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/constitutions/{constitution}: /constitutions/{constitution}:
parameters: parameters:
- $ref: '#/components/parameters/constitution' - $ref: '#/components/parameters/constitution'
@@ -955,6 +1118,9 @@ paths:
tags: tags:
- opinion - opinion
- citizen - citizen
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
responses: responses:
200: 200:
description: Opinions description: Opinions
@@ -969,6 +1135,13 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/Opinion' $ref: '#/components/schemas/Opinion'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/articles/{article}/opinions: /articles/{article}/opinions:
parameters: parameters:
- $ref: '#/components/parameters/article' - $ref: '#/components/parameters/article'
@@ -1021,6 +1194,12 @@ paths:
responses: responses:
201: 201:
description: Return only http status 201 on success description: Return only http status 201 on success
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/citizens/{citizen}/votes: /citizens/{citizen}/votes:
parameters: parameters:
- $ref: '#/components/parameters/citizen' - $ref: '#/components/parameters/citizen'
@@ -1079,6 +1258,12 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/VoteAggregation' $ref: '#/components/schemas/VoteAggregation'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/citizens/{citizen}/votes/articles: /citizens/{citizen}/votes/articles:
@@ -1092,6 +1277,9 @@ paths:
- vote - vote
- article - article
- citizen - citizen
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
responses: responses:
200: 200:
description: Votes description: Votes
@@ -1106,6 +1294,12 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/VoteResponse' $ref: '#/components/schemas/VoteResponse'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/articles/{article}/vote: /articles/{article}/vote:
@@ -1130,6 +1324,12 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/VoteAggregation' $ref: '#/components/schemas/VoteAggregation'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
@@ -1159,6 +1359,12 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/WorkgroupListing' $ref: '#/components/schemas/WorkgroupListing'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
post: post:
summary: Create new Workgroup summary: Create new Workgroup
security: security:
@@ -1197,6 +1403,12 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Workgroup' $ref: '#/components/schemas/Workgroup'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
/workgroups/{workgroup}: /workgroups/{workgroup}:
parameters: parameters:
- $ref: '#/components/parameters/workgroup' - $ref: '#/components/parameters/workgroup'
@@ -1245,6 +1457,12 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Workgroup' $ref: '#/components/schemas/Workgroup'
400:
description: BadReqest
content:
application/json:
schema:
$ref: '#/components/schemas/400'
delete: delete:
summary: Delete one workgroup summary: Delete one workgroup
security: security:
@@ -1373,6 +1591,17 @@ components:
- createdAt - createdAt
- vote - vote
- popularity - popularity
citizenSort:
name: sort
in: query
description: The sort field name
example: createdAt
required: false
schema:
type: string
enum:
- title
- createdAt
workgroupSort: workgroupSort:
name: sort name: sort
in: query in: query
@@ -1855,6 +2084,8 @@ components:
$ref: '#/components/schemas/UUID' $ref: '#/components/schemas/UUID'
title: title:
type: string type: string
minLength: 10
maxLength: 80
example: example:
Constitution for the liberty Constitution for the liberty
titles: titles:
@@ -1870,6 +2101,8 @@ components:
$ref: '#/components/schemas/UUID' $ref: '#/components/schemas/UUID'
name: name:
type: string type: string
minLength: 10
maxLength: 80
example: example:
The liberties The liberties
articles: articles:
@@ -2209,6 +2442,47 @@ components:
- REPORTER - REPORTER
example: MASTER example: MASTER
400:
description: Bad Request
required:
- title
- invalidParams
additionalProperties: false
properties:
statusCode:
type: integer
example: 400
title:
type: string
example: Bad Request
invalidParams:
type: array
items:
required:
- name
- reason
properties:
name:
type: string
example: '.title'
reason:
type: string
example: 'Cannot be null'
404:
description: Not Found
required:
- title
- statusCode
additionalProperties: false
properties:
statusCode:
type: integer
example: 404
title:
type: string
example: Bad Request
securitySchemes: securitySchemes:
JWTAuth: JWTAuth:
type: http type: http

View File

@@ -45,7 +45,7 @@ begin
case direction when 'asc' then case direction when 'asc' then
case sort case sort
when 'title' then a.title when 'title' then a.title
when 'created_at' then a.created_at::text when 'createdAt' then a.created_at::text
when 'vote' then ca.score::text when 'vote' then ca.score::text
when 'popularity' then ca.total::text when 'popularity' then ca.total::text
else null else null
@@ -54,7 +54,7 @@ begin
case direction when 'desc' then case direction when 'desc' then
case sort case sort
when 'title' then a.title when 'title' then a.title
when 'created_at' then a.created_at::text when 'createdAt' then a.created_at::text
when 'vote' then ca.score::text when 'vote' then ca.score::text
when 'popularity' then ca.total::text when 'popularity' then ca.total::text
end end

View File

@@ -23,14 +23,14 @@ begin
case direction when 'asc' then case direction when 'asc' then
case sort case sort
when 'name' then (z.name->'first_name')::text when 'name' then (z.name->'first_name')::text
when 'created_at' then z.created_at::text when 'createdAt' then z.created_at::text
else null else null
end end
end, end,
case direction when 'desc' then case direction when 'desc' then
case sort case sort
when 'name' then (z.name->'first_name')::text when 'name' then (z.name->'first_name')::text
when 'created_at' then z.created_at::text when 'createdAt' then z.created_at::text
end end
end end
desc, desc,

View File

@@ -1,4 +1,4 @@
create or replace function comment(reference regclass, resource json, out _id uuid) create or replace function comment(reference regclass, inout resource json)
language plpgsql as language plpgsql as
$$ $$
declare declare
@@ -17,7 +17,8 @@ begin
else else
raise exception 'comment with target as "%", is not implemented', reference::text; raise exception 'comment with target as "%", is not implemented', reference::text;
end if; end if;
_id = _new_id;
select find_comment_by_id(_new_id) into resource;
end; end;
$$; $$;

View File

@@ -1,9 +1,11 @@
create or replace function edit_comment(_id uuid, _content text) returns void create or replace function edit_comment(_id uuid, _content text, out resource json)
language plpgsql as language plpgsql as
$$ $$
begin begin
update comment c set update comment c set
"content" = _content "content" = _content
where c.id = _id; where c.id = _id;
select find_comment_by_id(_id) into resource;
end; end;
$$; $$;

View File

@@ -26,7 +26,7 @@ begin
else null else null
end desc, end desc,
case sort case sort
when 'created_at' then com.created_at::text when 'createdAt' then com.created_at::text
else null else null
end desc, end desc,
com.created_at desc com.created_at desc

View File

@@ -22,14 +22,14 @@ begin
case direction when 'asc' then case direction when 'asc' then
case sort case sort
when 'title' then c.title when 'title' then c.title
when 'created_at' then c.created_at::text when 'createdAt' then c.created_at::text
else null else null
end end
end, end,
case direction when 'desc' then case direction when 'desc' then
case sort case sort
when 'title' then c.title when 'title' then c.title
when 'created_at' then c.created_at::text when 'createdAt' then c.created_at::text
end end
end end
desc, desc,

View File

@@ -21,7 +21,7 @@ begin
f.created_at, f.created_at,
f.target_reference, f.target_reference,
json_build_object('id', f.target_id) as target, json_build_object('id', f.target_id) as target,
json_build_object('id', f.created_by_id) as created_by find_citizen_by_id_with_user(f.created_by_id) as created_by
from follow_article as f from follow_article as f
join article a on f.target_id = a.id join article a on f.target_id = a.id
where a.version_id = _version_id where a.version_id = _version_id

View File

@@ -2,7 +2,7 @@ create or replace function find_workgroups(
_search text default null, _search text default null,
_filter json default '{}', _filter json default '{}',
direction text default 'desc', direction text default 'desc',
sort text default 'created_at', sort text default 'createdAt',
"limit" int default 50, "limit" int default 50,
"offset" int default 0, "offset" int default 0,
out resource json, out resource json,
@@ -41,14 +41,14 @@ begin
case direction when 'asc' then case direction when 'asc' then
case sort case sort
when 'name' then w.name when 'name' then w.name
when 'created_at' then w.created_at::text when 'createdAt' then w.created_at::text
else null else null
end end
end, end,
case direction when 'desc' then case direction when 'desc' then
case sort case sort
when 'name' then w.name when 'name' then w.name
when 'created_at' then w.created_at::text when 'createdAt' then w.created_at::text
end end
end end
desc, desc,

View File

@@ -30,7 +30,7 @@ internal class NotificationsPushTest {
@BeforeAll @BeforeAll
@JvmStatic @JvmStatic
fun before() { fun before() {
val config: Configuration = Configuration("application-test.conf") val config = Configuration("application-test.conf")
RedisClient.create(config.redis).connect().sync().flushall() RedisClient.create(config.redis).connect().sync().flushall()
/* Purge rabbit notification queues */ /* Purge rabbit notification queues */
@@ -45,7 +45,7 @@ internal class NotificationsPushTest {
@Test @Test
fun `Notification from redis is well catch and return`() = runBlocking { fun `Notification from redis is well catch and return`() = runBlocking {
val config: Configuration = Configuration("application-test.conf") val config = Configuration("application-test.conf")
/* Redis client for test */ /* Redis client for test */
val redisClientTest = RedisClient.create(config.redis) val redisClientTest = RedisClient.create(config.redis)
@@ -74,7 +74,7 @@ internal class NotificationsPushTest {
} }
val notifAfterSubscribe = ArticleUpdateNotification(article) val notifAfterSubscribe = ArticleUpdateNotification(article)
/* init event for emulate incomint message from websocket */ /* init event for emulate incoming message from websocket */
val event = MutableSharedFlow<Notification>() val event = MutableSharedFlow<Notification>()
val incomingFlow = event.asSharedFlow() val incomingFlow = event.asSharedFlow()

View File

@@ -2,6 +2,7 @@ package functional
import fr.dcproject.application.Env.TEST import fr.dcproject.application.Env.TEST
import fr.dcproject.application.module import fr.dcproject.application.module
import fr.dcproject.common.utils.retry
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleViewRepository import fr.dcproject.component.article.database.ArticleViewRepository
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
@@ -20,6 +21,8 @@ import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
import java.util.UUID import java.util.UUID
import kotlin.time.ExperimentalTime
import kotlin.time.seconds
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI @KtorExperimentalAPI
@@ -27,6 +30,7 @@ import java.util.UUID
@TestInstance(PER_CLASS) @TestInstance(PER_CLASS)
@Tags(Tag("functional"), Tag("view")) @Tags(Tag("functional"), Tag("view"))
class ViewTest { class ViewTest {
@ExperimentalTime
@Test @Test
fun `test View Article`() { fun `test View Article`() {
val article = ArticleForView( val article = ArticleForView(
@@ -75,9 +79,8 @@ class ViewTest {
article article
) )
/* Sleep because ES is not sync ! */ /* Retry because ES is not sync ! */
Thread.sleep(1000) retry(10, 0.3.seconds) {
/* Get view */ /* Get view */
val afterView = viewRepository.getViewsCount(article) val afterView = viewRepository.getViewsCount(article)
@@ -86,4 +89,5 @@ class ViewTest {
afterView.unique `should be equal to` startView.unique + 3 afterView.unique `should be equal to` startView.unique + 3
} }
} }
}
} }

View File

@@ -1,5 +1,7 @@
package integration package integration
import fr.dcproject.common.utils.toUUID
import integration.steps.`when`.Validate
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
@@ -16,9 +18,11 @@ import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null` import integration.steps.then.`And the response should not be null`
import integration.steps.then.`And the response should not contain` import integration.steps.then.`And the response should not contain`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.`whish contains` import integration.steps.then.`which contains`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Forbidden import io.ktor.http.HttpStatusCode.Companion.Forbidden
import io.ktor.http.HttpStatusCode.Companion.NotFound
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Tags
@@ -32,13 +36,24 @@ class `Article routes` : BaseTest() {
fun `I can get article list`() { fun `I can get article list`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have articles`(3) `Given I have articles`(3)
`When I send a GET request`("/articles") `Then the response should be` OK and { `Given I have article`(createdBy = "ddb17f17-e8ab-4ada-bdf7-bfd6b0f1b5ed".toUUID())
`When I send a GET request`("/articles?page=1&limit=10&sort=title&createdBy=ddb17f17-e8ab-4ada-bdf7-bfd6b0f1b5ed") `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].createdBy.name.firstName", "firstName.+") `And the response should contain pattern`("$.result[0].createdBy.name.firstName", "firstName.+")
`And the response should contain pattern`("$.result[1].createdBy.name.firstName", "firstName.+") `And the response should not contain`("$.result[1]")
`And the response should contain pattern`("$.result[2].createdBy.name.firstName", "firstName.+") `And the response should contain list`("$.result", 1)
`And the response should not contain`("$.result[3]") }
`And the response should contain list`("$.result", 3) }
}
@Test
@Tag("BadRequest")
fun `I cannot get article list`() {
withIntegrationApplication {
`Given I have articles`(3)
`When I send a GET request`("/articles?page=1&limit=10&sort=title&createdBy=hello", Validate.ALL - Validate.REQUEST_PARAM) `Then the response should be` BadRequest and {
`And the response should contain`("$.invalidParams[*].name", ".createdBy")
`And the response should contain`("$.invalidParams[*].reason", "must be UUID")
} }
} }
} }
@@ -51,8 +66,8 @@ class `Article routes` : BaseTest() {
`Given I have article created by workgroup`("2bccd5a7-9082-4b31-88f8-e25d70b22b12") `Given I have article created by workgroup`("2bccd5a7-9082-4b31-88f8-e25d70b22b12")
`When I send a GET request`("/articles?workgroup=2bccd5a7-9082-4b31-88f8-e25d70b22b12") `Then the response should be` OK and { `When I send a GET request`("/articles?workgroup=2bccd5a7-9082-4b31-88f8-e25d70b22b12") `Then the response should be` OK and {
`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") `which contains` 1
`And have property`("$.result[0]workgroup.name") `whish contains` "Les papy" `And have property`("$.result[0]workgroup.name") `which contains` "Les papy"
} }
} }
} }
@@ -63,7 +78,31 @@ class `Article routes` : BaseTest() {
`Given I have article`(id = "65cda9f3-8991-4420-8d41-1da9da72c9bb") `Given I have article`(id = "65cda9f3-8991-4420-8d41-1da9da72c9bb")
`When I send a GET request`("/articles/65cda9f3-8991-4420-8d41-1da9da72c9bb") `Then the response should be` OK and { `When I send a GET request`("/articles/65cda9f3-8991-4420-8d41-1da9da72c9bb") `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And have property`("$.id") `whish contains` "65cda9f3-8991-4420-8d41-1da9da72c9bb" `And have property`("$.id") `which contains` "65cda9f3-8991-4420-8d41-1da9da72c9bb"
}
}
}
@Test
fun `I cannot get article with id doesn't exist`() {
withIntegrationApplication {
`When I send a GET request`("/articles/635fe2e8-2dbc-4c80-b306-101d38a4ab23") `Then the response should be` NotFound and {
`And the response should not be null`()
`And the response should contain`("$.title", "Article 635fe2e8-2dbc-4c80-b306-101d38a4ab23 not found")
`And the response should contain`("$.statusCode", 404)
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot get article by id with wrong id format`() {
withIntegrationApplication {
`Given I have article`(id = "65cda9f3-8991-4420-8d41-1da9da72c9bb")
`When I send a GET request`("/articles/abcd") `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", "ID")
`And the response should contain`("$.invalidParams[0].reason", "must be UUID")
} }
} }
} }
@@ -72,10 +111,36 @@ class `Article routes` : BaseTest() {
fun `I can get versions of article by the id`() { fun `I can get versions of article by the id`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have article`(id = "13e6091c-8fed-4600-b079-a97a6b7a9800") `Given I have article`(id = "13e6091c-8fed-4600-b079-a97a6b7a9800")
`When I send a GET request`("/articles/13e6091c-8fed-4600-b079-a97a6b7a9800/versions") `Then the response should be` OK and { `When I send a GET request`("/articles/13e6091c-8fed-4600-b079-a97a6b7a9800/versions?page=1&limit=10&sort=title") `Then the response should be` OK and {
`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") `which contains` 1
`And have property`("$.result[0].id") `whish contains` "13e6091c-8fed-4600-b079-a97a6b7a9800" `And have property`("$.result[0].id") `which contains` "13e6091c-8fed-4600-b079-a97a6b7a9800"
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot get versions of article by the id with wrong id`() {
withIntegrationApplication {
`Given I have article`(id = "13e6091c-8fed-4600-b079-a97a6b7a9800")
`When I send a GET request`("/articles/abcd/versions") `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".article")
`And the response should contain`("$.invalidParams[0].reason", "must be UUID")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot get versions of article by the id with wrong request`() {
withIntegrationApplication {
`Given I have article`(id = "13e6091c-8fed-4600-b079-a97a6b7a9800")
`When I send a GET request`("/articles/13e6091c-8fed-4600-b079-a97a6b7a9800/versions?page=1&limit=10&sort=wrong") `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".sort")
`And the response should contain pattern`("$.invalidParams[0].reason", "must be one of: ('[^']+'(, )?)+")
} }
} }
} }
@@ -92,8 +157,8 @@ class `Article routes` : BaseTest() {
"versionId": "09c418b6-63ba-448b-b38b-502b41cd500e", "versionId": "09c418b6-63ba-448b-b38b-502b41cd500e",
"title": "title2", "title": "title2",
"anonymous": false, "anonymous": false,
"content": "content2", "content": "Sed malesuada ante et sem congue, scelerisque feugiat lorem viverra.",
"description": "description2", "description": "Sed vulputate, ligula id porta posuere, sapien lorem mattis arcu, sit amet luctus erat orci sed tellus.",
"tags": [ "tags": [
"green" "green"
] ]
@@ -102,12 +167,13 @@ 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`("$.versionId") `whish contains` "09c418b6-63ba-448b-b38b-502b41cd500e" `And have property`("$.versionId") `which contains` "09c418b6-63ba-448b-b38b-502b41cd500e"
} }
} }
} }
@Test @Test
@Tag("Forbidden")
fun `I cannot create an article if I'm not connected`() { fun `I cannot create an article if I'm not connected`() {
withIntegrationApplication { withIntegrationApplication {
`When I send a POST request`("/articles") { `When I send a POST request`("/articles") {
@@ -117,8 +183,8 @@ class `Article routes` : BaseTest() {
"versionId": "e3c7ce42-241c-4caf-9a59-aba4e466440e", "versionId": "e3c7ce42-241c-4caf-9a59-aba4e466440e",
"title": "title2", "title": "title2",
"anonymous": false, "anonymous": false,
"content": "content2", "content": "Sed malesuada ante et sem congue, scelerisque feugiat lorem viverra.",
"description": "description2", "description": "Sed vulputate, ligula id porta posuere, sapien lorem mattis arcu, sit amet luctus erat orci sed tellus.",
"tags": [ "tags": [
"green" "green"
] ]
@@ -132,4 +198,35 @@ class `Article routes` : BaseTest() {
} }
} }
} }
@Test
@Tag("BadRequest")
fun `I cannot create an article with wrong request`() {
withIntegrationApplication {
`Given I have citizen`("John", "Doe")
`When I send a POST request`("/articles", Validate.NONE) {
`authenticated as`("John", "Doe")
`with body`(
"""
{
"versionId": "09c418b6-63ba-448b-b38b-502b41cd500e",
"title": "title2",
"anonymous": false,
"content": "content2",
"description": "description2",
"tags": [
"green"
]
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".content")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 50 characters")
`And the response should contain`("$.invalidParams[1].name", ".description")
`And the response should contain`("$.invalidParams[1].reason", "must have at least 50 characters")
}
}
}
} }

View File

@@ -9,7 +9,7 @@ import integration.steps.given.`authenticated as`
import integration.steps.then.`And have property` import integration.steps.then.`And have property`
import integration.steps.then.`And the response should not be null` import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.`whish contains` import integration.steps.then.`which contains`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
@@ -26,7 +26,7 @@ class `Citizen routes` : BaseTest() {
fun `I can get Citizens information`() { fun `I can get Citizens information`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Jean", "Perrin", id = "5267a5c6-af42-4a02-aa2b-6b71d2e43973") `Given I have citizen`("Jean", "Perrin", id = "5267a5c6-af42-4a02-aa2b-6b71d2e43973")
`When I send a GET request`("/citizens") { `When I send a GET request`("/citizens?page=1&limit=5&sort=createdAt") {
`authenticated as`("Jean", "Perrin") `authenticated as`("Jean", "Perrin")
} `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`()
@@ -34,6 +34,19 @@ class `Citizen routes` : BaseTest() {
} }
} }
@Test
@Tag("BadRequest")
fun `I cannot get Citizens information with wrong request`() {
withIntegrationApplication {
`Given I have citizen`("Jean", "Perrin", id = "5267a5c6-af42-4a02-aa2b-6b71d2e43973")
`When I send a GET request`("/citizens?page=1&limit=5&sort=created_at", Validate.ALL - Validate.REQUEST_PARAM) {
`authenticated as`("Jean", "Perrin")
} `Then the response should be` BadRequest and {
`And the response should not be null`()
}
}
}
@Test @Test
fun `I can get specific Citizen information`() { fun `I can get specific Citizen information`() {
withIntegrationApplication { withIntegrationApplication {
@@ -42,7 +55,7 @@ class `Citizen routes` : BaseTest() {
`authenticated as`("Linus", "Pauling") `authenticated as`("Linus", "Pauling")
} `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`("$.id") `whish contains` "47a05c0f-7329-46c3-a7d0-325db37e9114" `And have property`("$.id") `which contains` "47a05c0f-7329-46c3-a7d0-325db37e9114"
} }
} }
} }
@@ -55,7 +68,7 @@ class `Citizen routes` : BaseTest() {
`authenticated as`("Henri", "Becquerel") `authenticated as`("Henri", "Becquerel")
} `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`("$.id") `whish contains` "47356809-c8ef-4649-8b99-1c5cb9886d38" `And have property`("$.id") `which contains` "47356809-c8ef-4649-8b99-1c5cb9886d38"
} }
} }
} }
@@ -69,8 +82,8 @@ class `Citizen routes` : BaseTest() {
`with body`( `with body`(
""" """
{ {
"oldPassword": "azerty", "oldPassword": "Azerty123!",
"newPassword": "qwerty" "newPassword": "Qwerty123!"
} }
""" """
) )
@@ -79,6 +92,7 @@ class `Citizen routes` : BaseTest() {
} }
@Test @Test
@Tag("BadRequest")
fun `I cannot change my password if request is bad formatted`() { fun `I cannot change my password if request is bad formatted`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Louis", "Breguet", id = "6cf2a19d-d15d-4ee5-b2a9-907afd26b525") `Given I have citizen`("Louis", "Breguet", id = "6cf2a19d-d15d-4ee5-b2a9-907afd26b525")

View File

@@ -1,9 +1,11 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.`when`.Validate.ALL
import integration.steps.`when`.Validate.REQUEST_BODY
import integration.steps.`when`.Validate.REQUEST_PARAM
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
@@ -13,6 +15,7 @@ import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null` import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -33,14 +36,37 @@ class `Comment articles routes` : BaseTest() {
`with body`( `with body`(
""" """
{ {
"content": "Hello mister" "content": "Hello mister MARABOUTCHA"
} }
""" """
) )
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.target.id", "aa16c635-28da-46f0-9a89-934eef88c7ca") `And the response should contain`("$.target.id", "aa16c635-28da-46f0-9a89-934eef88c7ca")
`And the response should contain`("$.content", "Hello mister") `And the response should contain`("$.content", "Hello mister MARABOUTCHA")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot comment article with bad request`() {
withIntegrationApplication {
`Given I have citizen`("Michael", "Faraday")
`Given I have article`(id = "aa16c635-28da-46f0-9a89-934eef88c7ca")
`When I send a POST request`("/articles/aa16c635-28da-46f0-9a89-934eef88c7ca/comments", ALL - REQUEST_BODY) {
`authenticated as`("Michael", "Faraday")
`with body`(
"""
{
"content": "To small content"
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".content")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
} }
} }
} }
@@ -52,7 +78,7 @@ class `Comment articles routes` : BaseTest() {
`Given I have citizen`("Enrico", "Fermi") `Given I have citizen`("Enrico", "Fermi")
`Given I have article`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a") `Given I have article`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
`Given I have comment on article`(article = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi")) `Given I have comment on article`(article = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
`When I send a GET request`("/articles/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments") { `When I send a GET request`("/articles/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=votes") {
`authenticated as`("Enrico", "Fermi") `authenticated as`("Enrico", "Fermi")
} `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`()
@@ -61,6 +87,23 @@ class `Comment articles routes` : BaseTest() {
} }
} }
@Test
@Tag("BadRequest")
fun `I cannot get all comment on article with wrong parameters`() {
withIntegrationApplication {
`Given I have citizen`("Enrico", "Fermi")
`Given I have article`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
`Given I have comment on article`(article = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
`When I send a GET request`("/articles/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=wrong", ALL - REQUEST_PARAM) {
`authenticated as`("Enrico", "Fermi")
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[*].name", ".sort")
`And the response should contain`("$.invalidParams[*].reason", "must be one of: 'votes', 'createdAt'")
}
}
}
/* TODO add votes */ /* TODO add votes */
@Test @Test
fun `I can get all comment on article sorted by votes`() { fun `I can get all comment on article sorted by votes`() {
@@ -93,45 +136,4 @@ class `Comment articles routes` : BaseTest() {
} }
} }
} }
@Test
fun `I can edit comment`() {
withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves")
`Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1")
`Given I have comment on article`(article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1", createdBy = Name("Hubert", "Reeves"), id = "fd30d20f-656c-42c6-8955-f61c04537464")
`When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464") {
`authenticated as`("Hubert", "Reeves")
`with body`(
"""
{
"content": "Hello boy"
}
"""
)
} `Then the response should be` OK and {
`And the response should not be null`()
`And the response should contain`("$.content", "Hello boy")
}
}
}
@Test
fun `I can get comment by its ID`() {
withIntegrationApplication {
`Given I have citizen`("Alfred", "Kastler")
`Given I have article`(id = "3897465b-19d2-43a0-86ea-1e29dbb11ec9")
`Given I have comment on article`(
article = "3897465b-19d2-43a0-86ea-1e29dbb11ec9",
createdBy = Name("Alfred", "Kastler"),
id = "edd296a8-fc7a-4717-a2bb-9f035ceca3c2",
content = "Hello boy"
)
`When I send a GET request`("/comments/edd296a8-fc7a-4717-a2bb-9f035ceca3c2") {
} `Then the response should be` OK and {
`And the response should not be null`()
`And the response should contain`("$.content", "Hello boy")
}
}
}
} }

View File

@@ -1,6 +1,9 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.`when`.Validate
import integration.steps.`when`.Validate.ALL
import integration.steps.`when`.Validate.REQUEST_BODY
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
@@ -13,6 +16,7 @@ import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null` import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -33,12 +37,69 @@ class `Comment constitutions routes` : BaseTest() {
`with body`( `with body`(
""" """
{ {
"content": "Hello mister" "content": "Hello mister MARABOUTCHA"
} }
""" """
) )
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.target.id", "1707c287-a472-4a62-89f2-9e85030e915c")
`And the response should contain`("$.content", "Hello mister MARABOUTCHA")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot comment constitution with bad request`() {
withIntegrationApplication {
`Given I have citizen`("Nicolas", "Copernic")
`Given I have constitution`(id = "aa16c635-28da-46f0-9a89-934eef88c7ca")
`When I send a POST request`("/constitutions/aa16c635-28da-46f0-9a89-934eef88c7ca/comments", ALL - REQUEST_BODY) {
`authenticated as`("Nicolas", "Copernic")
`with body`(
"""
{
"content": "To small content"
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".content")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
}
}
}
@Test
fun `I can get all comment on constitution`() {
withIntegrationApplication {
`Given I have citizen`("Enrico", "Fermi")
`Given I have constitution`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
`Given I have comment on constitution`(constitution = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
`When I send a GET request`("/constitutions/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=votes") {
`authenticated as`("Enrico", "Fermi")
} `Then the response should be` OK and {
`And the response should not be null`()
`And the response should contain`("$.result[0].target.id", "6166c078-ca97-4366-b0aa-2a5cd558c78a")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot get all comment on constitution with wrong parameters`() {
withIntegrationApplication {
`Given I have citizen`("Enrico", "Fermi")
`Given I have constitution`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
`Given I have comment on constitution`(constitution = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
`When I send a GET request`("/constitutions/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=wrong", ALL - Validate.REQUEST_PARAM) {
`authenticated as`("Enrico", "Fermi")
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[*].name", ".sort")
`And the response should contain`("$.invalidParams[*].reason", "must be one of: 'votes', 'createdAt'")
} }
} }
} }

View File

@@ -1,13 +1,23 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI
import integration.steps.`when`.Validate.ALL
import integration.steps.`when`.Validate.REQUEST_BODY
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have comment on article` import integration.steps.given.`Given I have comment on article`
import integration.steps.given.`Given I have comment on comment`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null` import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Tags
@@ -30,4 +40,126 @@ class `Comment routes` : BaseTest() {
} }
} }
} }
@Test
fun `I can create comment`() {
withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves")
`Given I have comment on comment`(id = "49933147-fc0f-4e5c-aa8d-f77fa0d88fa6")
`When I send a POST request`("/comments/49933147-fc0f-4e5c-aa8d-f77fa0d88fa6") {
`authenticated as`("Hubert", "Reeves")
`with body`(
"""
{
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}
"""
)
} `Then the response should be` Created and {
`And the response should not be null`()
`And the response should contain`("$.content", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot create comment with bad request`() {
withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves")
`Given I have comment on comment`(id = "49933147-fc0f-4e5c-aa8d-f77fa0d88fa6")
`When I send a POST request`("/comments/49933147-fc0f-4e5c-aa8d-f77fa0d88fa6", ALL - REQUEST_BODY) {
`authenticated as`("Hubert", "Reeves")
`with body`(
"""
{
"content": "small content"
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".content")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
}
}
}
@Test
fun `I can edit comment`() {
withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves")
`Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1")
`Given I have comment on article`(
article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1",
createdBy = CitizenI.Name(
"Hubert",
"Reeves"
),
id = "fd30d20f-656c-42c6-8955-f61c04537464"
)
`When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464") {
`authenticated as`("Hubert", "Reeves")
`with body`(
"""
{
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}
"""
)
} `Then the response should be` OK and {
`And the response should not be null`()
`And the response should contain`("$.content", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
}
}
}
@Test
fun `I cannot edit comment with bad request`() {
withIntegrationApplication {
`Given I have citizen`("Hubert", "Reeves")
`Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1")
`Given I have comment on article`(
article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1",
createdBy = CitizenI.Name(
"Hubert",
"Reeves"
),
id = "fd30d20f-656c-42c6-8955-f61c04537464"
)
`When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464", ALL - REQUEST_BODY) {
`authenticated as`("Hubert", "Reeves")
`with body`(
"""
{
"content": "small content"
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".content")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
}
}
}
@Test
fun `I can get comment by its ID`() {
withIntegrationApplication {
`Given I have citizen`("Alfred", "Kastler")
`Given I have article`(id = "3897465b-19d2-43a0-86ea-1e29dbb11ec9")
`Given I have comment on article`(
article = "3897465b-19d2-43a0-86ea-1e29dbb11ec9",
createdBy = CitizenI.Name("Alfred", "Kastler"),
id = "edd296a8-fc7a-4717-a2bb-9f035ceca3c2",
content = "Hello boy"
)
`When I send a GET request`("/comments/edd296a8-fc7a-4717-a2bb-9f035ceca3c2") {
} `Then the response should be` OK and {
`And the response should not be null`()
`And the response should contain`("$.content", "Hello boy")
}
}
}
} }

View File

@@ -1,6 +1,8 @@
package integration package integration
import integration.steps.`when`.Validate import integration.steps.`when`.Validate.ALL
import integration.steps.`when`.Validate.REQUEST_BODY
import integration.steps.`when`.Validate.REQUEST_PARAM
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
@@ -9,9 +11,10 @@ import integration.steps.given.`Given I have constitution`
import integration.steps.given.`Given I have constitutions` import integration.steps.given.`Given I have constitutions`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And have property` import integration.steps.then.`And have property`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null` import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.`whish contains` import integration.steps.then.`which contains`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
@@ -28,12 +31,25 @@ class `Constitution routes` : BaseTest() {
fun `I can get constitution list`() { fun `I can get constitution list`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have constitutions`(3) `Given I have constitutions`(3)
`When I send a GET request`("/constitutions") `Then the response should be` OK and { `When I send a GET request`("/constitutions?page=1&limit=10&sort=title&direction=desc") `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
} }
} }
} }
@Test
@Tag("BadRequest")
fun `I cannot get constitution list with wrong request`() {
withIntegrationApplication {
`Given I have constitutions`(3)
`When I send a GET request`("/constitutions?page=1&limit=5000&sort=title&direction=desc", ALL - REQUEST_PARAM) `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".limit")
`And the response should contain`("$.invalidParams[0].reason", "must be at most '50'")
}
}
}
@Test @Test
fun `I can get constitution by ID`() { fun `I can get constitution by ID`() {
withIntegrationApplication { withIntegrationApplication {
@@ -41,7 +57,7 @@ class `Constitution routes` : BaseTest() {
`Given I have constitution`("0321c8d1-4ce3-4763-b5f4-a92611d280b4") `Given I have constitution`("0321c8d1-4ce3-4763-b5f4-a92611d280b4")
`When I send a GET request`("/constitutions/0321c8d1-4ce3-4763-b5f4-a92611d280b4") `Then the response should be` OK and { `When I send a GET request`("/constitutions/0321c8d1-4ce3-4763-b5f4-a92611d280b4") `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And have property`("$.id") `whish contains` "0321c8d1-4ce3-4763-b5f4-a92611d280b4" `And have property`("$.id") `which contains` "0321c8d1-4ce3-4763-b5f4-a92611d280b4"
} }
} }
} }
@@ -70,11 +86,11 @@ class `Constitution routes` : BaseTest() {
""" """
{ {
"versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e", "versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e",
"title":"Hello world!", "title":"Cras sit amet sapien mattis nulla rutrum blandit.",
"anonymous":true, "anonymous":true,
"titles":[ "titles":[
{ {
"name":"plop" "name":"Cras sit amet sapien mattis nulla rutrum blandit."
} }
] ]
} }
@@ -82,17 +98,18 @@ class `Constitution routes` : BaseTest() {
) )
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should not be null`() `And the response should not be null`()
`And have property`("$.versionId") `whish contains` "15814bb6-8d90-4c6a-a456-c3939a8ec75e" `And have property`("$.versionId") `which contains` "15814bb6-8d90-4c6a-a456-c3939a8ec75e"
`And have property`("$.title") `whish contains` "Hello world!" `And have property`("$.title") `which contains` "Cras sit amet sapien mattis nulla rutrum blandit."
} }
} }
} }
@Test @Test
@Tag("BadRequest")
fun `I cannot create an constitution if bad request`() { fun `I cannot create an constitution if bad request`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Henri", "Poincaré") `Given I have citizen`("Henri", "Poincaré")
`When I send a POST request`("/constitutions", Validate.ALL - Validate.REQUEST_BODY) { `When I send a POST request`("/constitutions", ALL - REQUEST_BODY) {
`authenticated as`("Henri", "Poincaré") `authenticated as`("Henri", "Poincaré")
`with body`( `with body`(
""" """
@@ -112,4 +129,34 @@ class `Constitution routes` : BaseTest() {
} `Then the response should be` BadRequest } `Then the response should be` BadRequest
} }
} }
@Test
@Tag("BadRequest")
fun `I cannot create an constitution if request is not valid`() {
withIntegrationApplication {
`Given I have citizen`("Henri", "Poincaré")
`When I send a POST request`("/constitutions", ALL - REQUEST_BODY) {
`authenticated as`("Henri", "Poincaré")
`with body`(
"""
{
"versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e",
"title":"too small",
"anonymous":true,
"titles":[
{
"name":"too small"
}
]
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should contain`("$.invalidParams[0].name", ".title")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 10 characters")
`And the response should contain`("$.invalidParams[1].name", ".titles[0].name")
`And the response should contain`("$.invalidParams[1].reason", "must have at least 10 characters")
}
}
}
} }

View File

@@ -27,7 +27,7 @@ class `Login routes` : BaseTest() {
""" """
{ {
"username": "niels-bohr", "username": "niels-bohr",
"password": "azerty" "password": "Azerty123!"
} }
""" """
) )

View File

@@ -0,0 +1,78 @@
package integration
import fr.dcproject.common.utils.toUUID
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.Name
import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.Notification
import fr.dcproject.component.notification.Publisher
import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have follow on article`
import integration.steps.given.`authenticated in url as`
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import kotlinx.coroutines.launch
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.koin.test.get
import kotlin.test.assertEquals
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("integration"), Tag("notification"))
class `Notification routes` : BaseTest() {
@Test
fun `I can send notification`() {
withIntegrationApplication {
`Given I have citizen`("John", "Doe", id = "1a34191a-9cde-45ba-8ac1-230138a102d3")
`Given I have article`(id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", createdBy = Name(firstName = "John", lastName = "Doe"))
`Given I have follow on article`("John", "Doe", article = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4")
val notification = ArticleUpdateNotification(
ArticleForView(
id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4".toUUID(),
title = "MyTitle",
content = "myContent",
description = "myDescription",
createdBy = CitizenCreator(
id = "1a34191a-9cde-45ba-8ac1-230138a102d3".toUUID(),
name = Name(firstName = "John", lastName = "Doe"),
email = "john-doe@plop.com",
user = UserCreator(username = "john-doe"),
)
)
)
val publisher = get<Publisher>()
launch {
publisher
.publish(notification)
.await()
}
Thread.sleep(1000)
handleWebSocketConversation(
"/notifications",
{
`authenticated in url as`("John", "Doe")
}
) { incoming, outgoing ->
incoming.receive().let {
when (it) {
is Frame.Text -> Notification.fromString<ArticleUpdateNotification>(it.readText()).let { notif ->
assertEquals(
"a06cbfb7-3094-4d64-aaa1-7486c0c292f4",
notif.target.id.toString()
)
outgoing.send(it)
}
else -> error(it.toString())
}
}
}
}
}
}

View File

@@ -1,6 +1,8 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.`when`.Validate.ALL
import integration.steps.`when`.Validate.REQUEST_PARAM
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a PUT request` import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
@@ -11,8 +13,10 @@ import integration.steps.given.`Given I have opinion on article`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain list` import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain` import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -133,7 +137,7 @@ class `Opinion routes` : BaseTest() {
article = "8651b530-ac1b-4214-a784-706781371074", article = "8651b530-ac1b-4214-a784-706781371074",
Name("Albert", "Einstein") Name("Albert", "Einstein")
) )
`When I send a GET request`("/citizens/c1542096-3431-432d-8e35-9dc071d4c818/opinions/articles") { `When I send a GET request`("/citizens/c1542096-3431-432d-8e35-9dc071d4c818/opinions/articles?page=1&limit=10") {
`authenticated as`("Albert", "Einstein") `authenticated as`("Albert", "Einstein")
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain`("$.result[0].name", "Opinion9") `And the response should contain`("$.result[0].name", "Opinion9")
@@ -141,4 +145,26 @@ class `Opinion routes` : BaseTest() {
} }
} }
} }
@Test
@Tags(Tag("article"), Tag("BadRequest"))
fun `I cannot get all my opinion of one article with wrong request`() {
withIntegrationApplication {
`Given I have citizen`("Albert", "Einstein", id = "c1542096-3431-432d-8e35-9dc071d4c818")
`Given I have an opinion choice`("Opinion9")
`Given I have article`("8651b530-ac1b-4214-a784-706781371074")
`Given I have opinion on article`(
"Opinion9",
article = "8651b530-ac1b-4214-a784-706781371074",
Name("Albert", "Einstein")
)
`When I send a GET request`("/citizens/c1542096-3431-432d-8e35-9dc071d4c818/opinions/articles?page=1&limit=60", ALL - REQUEST_PARAM) {
`authenticated as`("Albert", "Einstein")
} `Then the response should be` HttpStatusCode.BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".limit")
`And the response should contain`("$.invalidParams[0].reason", "must be at most '50'")
}
}
}
} }

View File

@@ -29,7 +29,7 @@ class `Register routes` : BaseTest() {
"birthday": "2001-01-01", "birthday": "2001-01-01",
"user":{ "user":{
"username": "george-junior", "username": "george-junior",
"password": "azerty" "password": "Azerty123!"
}, },
"email": "george-junior@gmail.com" "email": "george-junior@gmail.com"
} }

View File

@@ -1,6 +1,9 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.`when`.Validate.ALL
import integration.steps.`when`.Validate.REQUEST_BODY
import integration.steps.`when`.Validate.REQUEST_PARAM
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a PUT request` import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
@@ -12,53 +15,135 @@ import integration.steps.given.`Given I have vote +1 on article`
import integration.steps.given.`Given I have vote -1 on article` import integration.steps.given.`Given I have vote -1 on article`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain` import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestFactory
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("integration"), Tag("vote")) @Tags(Tag("integration"), Tag("vote"))
class `Vote routes` : BaseTest() { class `Vote routes` : BaseTest() {
@Test @TestFactory
fun `I can vote article`() { fun `I can vote article`(): List<DynamicTest> {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Thalès", "Milet") `Given I have citizen`("Thalès", "Milet")
`Given I have article`(id = "835c5101-ca39-4038-a4e6-da6ee62ca6d5") `Given I have article`(id = "835c5101-ca39-4038-a4e6-da6ee62ca6d5")
}
return (-1..1).map { note ->
DynamicTest.dynamicTest("""I can vote article with note "$note"""") {
withIntegrationApplication {
`When I send a PUT request`("/articles/835c5101-ca39-4038-a4e6-da6ee62ca6d5/vote") { `When I send a PUT request`("/articles/835c5101-ca39-4038-a4e6-da6ee62ca6d5/vote") {
`authenticated as`("Thalès", "Milet") `authenticated as`("Thalès", "Milet")
`with body`( `with body`(
""" """
{ {
"note": 1 "note": $note
} }
""" """
) )
} `Then the response should be` Created } `Then the response should be` Created
} }
} }
}
}
@Test @TestFactory
fun `I can vote constitution`() { @Tag("BadRequest")
fun `I cannot vote article with wrong request`(): List<DynamicTest> {
withIntegrationApplication {
`Given I have citizen`("Thalès", "Milet")
`Given I have article`(id = "835c5101-ca39-4038-a4e6-da6ee62ca6d5")
}
return listOf(-10, -2, +2, +10).map { note ->
DynamicTest.dynamicTest("""I can vote article with note "$note"""") {
withIntegrationApplication {
`When I send a PUT request`(
"/articles/835c5101-ca39-4038-a4e6-da6ee62ca6d5/vote",
ALL - REQUEST_BODY
) {
`authenticated as`("Thalès", "Milet")
`with body`(
"""
{
"note": $note
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".note")
`And the response should contain`("$.invalidParams[0].reason", if (note > 0) "must be at most '1'" else "must be at least '-1'")
}
}
}
}
}
@TestFactory
fun `I can vote constitution`(): List<DynamicTest> {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Gregor", "Mendel") `Given I have citizen`("Gregor", "Mendel")
`Given I have constitution`(id = "76e79c89-efc1-492d-9e8f-dc9717363a11") `Given I have constitution`(id = "76e79c89-efc1-492d-9e8f-dc9717363a11")
}
return (-1..1).map { note ->
DynamicTest.dynamicTest("""I can vote constitution with note "$note"""") {
withIntegrationApplication {
`When I send a PUT request`("/constitutions/76e79c89-efc1-492d-9e8f-dc9717363a11/vote") { `When I send a PUT request`("/constitutions/76e79c89-efc1-492d-9e8f-dc9717363a11/vote") {
`authenticated as`("Gregor", "Mendel") `authenticated as`("Gregor", "Mendel")
`with body`( `with body`(
""" """
{ {
"note": 1 "note": $note
}
""" """
) )
} `Then the response should be` Created } `Then the response should be` Created
} }
} }
}
}
@TestFactory
@Tag("BadRequest")
fun `I cannot vote constitution with wrong request`(): List<DynamicTest> {
withIntegrationApplication {
`Given I have citizen`("Gregor", "Mendel")
`Given I have constitution`(id = "76e79c89-efc1-492d-9e8f-dc9717363a11")
}
return listOf(-10, -2, +2, +10).map { note ->
DynamicTest.dynamicTest("""I can vote constitution with note "$note"""") {
withIntegrationApplication {
`When I send a PUT request`(
"/constitutions/76e79c89-efc1-492d-9e8f-dc9717363a11/vote",
ALL - REQUEST_BODY
) {
`authenticated as`("Gregor", "Mendel")
`with body`(
"""
{
"note": $note
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".note")
`And the response should contain`("$.invalidParams[0].reason", if (note > 0) "must be at most '1'" else "must be at least '-1'")
}
}
}
}
}
@Test @Test
fun `I can get votes of current citizen`() { fun `I can get votes of current citizen`() {
@@ -66,7 +151,7 @@ class `Vote routes` : BaseTest() {
`Given I have citizen`("Carl", "Gauss", id = "c044823d-e778-4256-9016-b1334bf933d3") `Given I have citizen`("Carl", "Gauss", id = "c044823d-e778-4256-9016-b1334bf933d3")
`Given I have article`("7c9286db-470d-448c-aab1-3f0b072213b1") `Given I have article`("7c9286db-470d-448c-aab1-3f0b072213b1")
`Given I have vote +1 on article`("7c9286db-470d-448c-aab1-3f0b072213b1", Name("Carl", "Gauss")) `Given I have vote +1 on article`("7c9286db-470d-448c-aab1-3f0b072213b1", Name("Carl", "Gauss"))
`When I send a GET request`("/citizens/c044823d-e778-4256-9016-b1334bf933d3/votes/articles") { `When I send a GET request`("/citizens/c044823d-e778-4256-9016-b1334bf933d3/votes/articles?page=1&limit=50") {
`authenticated as`("Carl", "Gauss") `authenticated as`("Carl", "Gauss")
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain`("$.currentPage", 1) `And the response should contain`("$.currentPage", 1)
@@ -77,6 +162,23 @@ class `Vote routes` : BaseTest() {
} }
} }
@Test
@Tag("BadRequest")
fun `I cannot get votes of current citizen with wrong request`() {
withIntegrationApplication {
`Given I have citizen`("Carl", "Gauss", id = "c044823d-e778-4256-9016-b1334bf933d3")
`Given I have article`("7c9286db-470d-448c-aab1-3f0b072213b1")
`Given I have vote +1 on article`("7c9286db-470d-448c-aab1-3f0b072213b1", Name("Carl", "Gauss"))
`When I send a GET request`("/citizens/c044823d-e778-4256-9016-b1334bf933d3/votes/articles?page=1&limit=60", ALL - REQUEST_PARAM) {
`authenticated as`("Carl", "Gauss")
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".limit")
`And the response should contain`("$.invalidParams[0].reason", "must be at most '50'")
}
}
}
@Test @Test
fun `I can get votes of current citizen by target ids`() { fun `I can get votes of current citizen by target ids`() {
withIntegrationApplication { withIntegrationApplication {
@@ -118,4 +220,39 @@ class `Vote routes` : BaseTest() {
} }
} }
} }
@TestFactory
@Tag("BadRequest")
fun `I cannot vote comment with wrong request`(): List<DynamicTest> {
withIntegrationApplication {
`Given I have citizen`("Antoine", "Lavoisier")
`Given I have article`(id = "835c5101-ca39-4038-a4e6-da6ee62ca6d5")
`Given I have comment on article`(
createdBy = Name("Antoine", "Lavoisier"),
article = "835c5101-ca39-4038-a4e6-da6ee62ca6d5",
id = "e793eccc-456b-4450-a292-46d592229b74",
)
}
return listOf(-10, -2, +2, +10).map { note ->
DynamicTest.dynamicTest("""I can vote comment with note "$note"""") {
withIntegrationApplication {
`When I send a PUT request`("/comments/e793eccc-456b-4450-a292-46d592229b74/vote", ALL - REQUEST_BODY) {
`authenticated as`("Antoine", "Lavoisier")
`with body`(
"""
{
"note": $note
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".note")
`And the response should contain`("$.invalidParams[0].reason", if (note > 0) "must be at most '1'" else "must be at least '-1'")
}
}
}
}
}
} }

View File

@@ -0,0 +1,118 @@
package integration
import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.`when`.`When I send a DELETE request`
import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have workgroup`
import integration.steps.given.`With members`
import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`Then the response should be`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("integration"), Tag("workgroup"), Tag("workgroupMember"))
class `Workgroup Members routes` : BaseTest() {
@Test
fun `I can add member to workgroup`() {
withIntegrationApplication {
`Given I have citizen`("Blaise", "Pascal")
`Given I have citizen`("Roger", "Penrose", id = "6d883fe7-5fc0-4a50-8858-72230673eba4")
`Given I have citizen`("Alessandro", "Volta", id = "b5bac515-45d4-4aeb-9b6d-2627a0bbc419")
`Given I have workgroup`("b0ea1922-3bc6-44e2-aa7c-40158998cfbb", createdBy = Name("Blaise", "Pascal"))
`When I send a POST request`("/workgroups/b0ea1922-3bc6-44e2-aa7c-40158998cfbb/members") {
`authenticated as`("Blaise", "Pascal")
`with body`(
"""
[
{
"citizen": {"id":"6d883fe7-5fc0-4a50-8858-72230673eba4"},
"roles": ["MASTER"]
},
{
"citizen": {"id":"b5bac515-45d4-4aeb-9b6d-2627a0bbc419"},
"roles": ["MASTER"]
}
]
"""
)
} `Then the response should be` Created
}
}
@Test
fun `I can remove member to workgroup`() {
withIntegrationApplication {
`Given I have citizen`("Heinrich", "Hertz", id = "94f92424-c257-4582-907c-98564a8c4ac9")
`Given I have citizen`("William", "Thomson", id = "87909ba3-2069-431c-9924-219fd8411cf2")
`Given I have citizen`("Paul", "Dirac", id = "1baf48bb-02bc-4d8f-ac86-33335354f5e7")
`Given I have workgroup`("b6c975df-dd44-4e99-adc1-f605746b0e11", createdBy = Name("Heinrich", "Hertz")) {
`With members`(
Name("William", "Thomson"),
Name("Paul", "Dirac"),
)
}
`When I send a DELETE request`("/workgroups/b6c975df-dd44-4e99-adc1-f605746b0e11/members") {
`authenticated as`("Heinrich", "Hertz")
"""
[
{
"citizen": {"id":"87909ba3-2069-431c-9924-219fd8411cf2"}
}
]
"""
} `Then the response should be` OK and {
`And the response should contain list`("$", 2)
`And the response should contain`("$.[0]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9")
`And the response should contain`("$.[1]citizen.id", "1baf48bb-02bc-4d8f-ac86-33335354f5e7")
}
}
}
@Test
fun `I can update members on workgroup`() {
withIntegrationApplication {
`Given I have citizen`("Leon", "Foucault")
`Given I have citizen`("Sadi", "Carnot", id = "be3b0926-8628-4426-804a-75188a6eb315")
`Given I have citizen`("Joseph", "Fourier", id = "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1")
`Given I have citizen`("Georg", "Ohm")
`Given I have workgroup`("784fe6bc-7635-4ae2-b080-3a4743b998bf", createdBy = Name("Leon", "Foucault")) {
`With members`(
Name("Sadi", "Carnot"),
Name("Joseph", "Fourier"),
)
}
`When I send a PUT request`("/workgroups/784fe6bc-7635-4ae2-b080-3a4743b998bf/members") {
`authenticated as`("Leon", "Foucault")
`with body`(
"""
[
{
"citizen": {"id":"be3b0926-8628-4426-804a-75188a6eb315"},
"roles": ["MASTER"]
},
{
"citizen": {"id":"b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1"},
"roles": ["MASTER"]
}
]
"""
)
} `Then the response should be` OK and {
`And the response should contain list`("$", 2)
`And the response should contain`("$.[0]citizen.id", "be3b0926-8628-4426-804a-75188a6eb315")
`And the response should contain`("$.[1]citizen.id", "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1")
}
}
}
}

View File

@@ -1,6 +1,8 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.`when`.Validate.REQUEST_BODY
import integration.steps.`when`.Validate.REQUEST_PARAM
import integration.steps.`when`.`When I send a DELETE request` import integration.steps.`when`.`When I send a DELETE request`
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
@@ -15,8 +17,10 @@ import integration.steps.then.`And have property`
import integration.steps.then.`And the response should be null` import integration.steps.then.`And the response should be null`
import integration.steps.then.`And the response should contain list` import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain` import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be` import integration.steps.then.`Then the response should be`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.NoContent import io.ktor.http.HttpStatusCode.Companion.NoContent
import io.ktor.http.HttpStatusCode.Companion.NotFound import io.ktor.http.HttpStatusCode.Companion.NotFound
@@ -73,7 +77,7 @@ class `Workgroup routes` : BaseTest() {
{ {
"id":"f496d86d-6654-4068-91ff-90e1dbcc5f38", "id":"f496d86d-6654-4068-91ff-90e1dbcc5f38",
"name":"Les Bouffons", "name":"Les Bouffons",
"description":"La vie est belle", "description":"Pellentesque eleifend malesuada aliquam. Maecenas et urna quis nunc lacinia scelerisque.",
"anonymous":false "anonymous":false
} }
""" """
@@ -81,7 +85,7 @@ class `Workgroup routes` : BaseTest() {
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should contain`("$.id", "f496d86d-6654-4068-91ff-90e1dbcc5f38") `And the response should contain`("$.id", "f496d86d-6654-4068-91ff-90e1dbcc5f38")
`And the response should contain`("$.name", "Les Bouffons") `And the response should contain`("$.name", "Les Bouffons")
`And the response should contain`("$.description", "La vie est belle") `And the response should contain`("$.description", "Pellentesque eleifend malesuada aliquam. Maecenas et urna quis nunc lacinia scelerisque.")
`And the response should contain`("$.anonymous", false) `And the response should contain`("$.anonymous", false)
} }
@@ -91,6 +95,36 @@ class `Workgroup routes` : BaseTest() {
} }
} }
@Test
@Tag("BadRequest")
fun `I cannot create a workgroup with wrong request`() {
withIntegrationApplication {
`Given I have citizen`("Werner", "Heisenberg")
`When I send a POST request`("/workgroups") {
`authenticated as`("Werner", "Heisenberg")
`with body`(
"""
{
"id":"f496d86d-6654-4068-91ff-90e1dbcc5f38",
"name":"sm",
"description":"small",
"anonymous":false,
"logo": "www.plop.com"
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".name")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 5 characters")
`And the response should contain`("$.invalidParams[1].name", ".description")
`And the response should contain`("$.invalidParams[1].reason", "must have at least 50 characters")
`And the response should contain`("$.invalidParams[2].name", ".logo")
`And the response should contain`("$.invalidParams[2].reason", "is not url")
}
}
}
@Test @Test
fun `I can edit a workgroup`() { fun `I can edit a workgroup`() {
withIntegrationApplication { withIntegrationApplication {
@@ -109,14 +143,15 @@ class `Workgroup routes` : BaseTest() {
""" """
{ {
"name":"La ratatouille", "name":"La ratatouille",
"description":"Une petite souris" "description":"Une petite souris avec un chapeau et qui aime la cuisine",
"logo": "http://sdf@exemple.com/sdfsd?sdf=sss"
} }
""" """
) )
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain`("$.id", "aa875a24-0050-4252-9130-d37391714e26") `And the response should contain`("$.id", "aa875a24-0050-4252-9130-d37391714e26")
`And the response should contain`("$.name", "La ratatouille") `And the response should contain`("$.name", "La ratatouille")
`And the response should contain`("$.description", "Une petite souris") `And the response should contain`("$.description", "Une petite souris avec un chapeau et qui aime la cuisine")
`And have property`("$.members") `And have property`("$.members")
`And the response should contain list`("$.members", 3) `And the response should contain list`("$.members", 3)
@@ -129,7 +164,43 @@ class `Workgroup routes` : BaseTest() {
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain`("$.id", "aa875a24-0050-4252-9130-d37391714e26") `And the response should contain`("$.id", "aa875a24-0050-4252-9130-d37391714e26")
`And the response should contain`("$.name", "La ratatouille") `And the response should contain`("$.name", "La ratatouille")
`And the response should contain`("$.description", "Une petite souris") `And the response should contain`("$.description", "Une petite souris avec un chapeau et qui aime la cuisine")
}
}
}
@Test
@Tag("BadRequest")
fun `I cannot edit a workgroup with bad request`() {
withIntegrationApplication {
`Given I have citizen`("John", "Wheeler")
`Given I have citizen`("Heinrich", "Hertz", id = "94f92424-c257-4582-907c-98564a8c4ac9")
`Given I have citizen`("William", "Thomson", id = "87909ba3-2069-431c-9924-219fd8411cf2")
`Given I have workgroup`("aa875a24-0050-4252-9130-d37391714e26", createdBy = Name("John", "Wheeler")) {
`With members`(
Name("Heinrich", "Hertz"),
Name("William", "Thomson"),
)
}
`When I send a PUT request`("/workgroups/aa875a24-0050-4252-9130-d37391714e26", -REQUEST_BODY) {
`authenticated as`("John", "Wheeler")
`with body`(
"""
{
"name":"sm",
"description":"small2",
"logo": "ws://sdfs.sdok"
}
"""
)
} `Then the response should be` BadRequest and {
`And the response should not be null`()
`And the response should contain`("$.invalidParams[0].name", ".name")
`And the response should contain`("$.invalidParams[0].reason", "must have at least 5 characters")
`And the response should contain`("$.invalidParams[1].name", ".description")
`And the response should contain`("$.invalidParams[1].reason", "must have at least 50 characters")
`And the response should contain`("$.invalidParams[2].name", ".logo")
`And the response should contain`("$.invalidParams[2].reason", "is not url")
} }
} }
} }
@@ -157,7 +228,7 @@ class `Workgroup routes` : BaseTest() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Max", "Planck") `Given I have citizen`("Max", "Planck")
`Given I have workgroup`("3fd8edb6-c4b4-4c94-bc75-ddd9b290d32c") `Given I have workgroup`("3fd8edb6-c4b4-4c94-bc75-ddd9b290d32c")
`When I send a GET request`("/workgroups") { `When I send a GET request`("/workgroups?page=1&limit=10&sort=createdAt") {
`authenticated as`("Max", "Planck") `authenticated as`("Max", "Planck")
`with no content`() `with no content`()
} `Then the response should be` OK and { } `Then the response should be` OK and {
@@ -167,94 +238,15 @@ class `Workgroup routes` : BaseTest() {
} }
@Test @Test
fun `I can add member to workgroup`() { @Tag("BadRequest")
fun `I cannot get workgroups list with wrong request`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Blaise", "Pascal") `Given I have workgroup`("3fd8edb6-c4b4-4c94-bc75-ddd9b290d32c")
`Given I have citizen`("Roger", "Penrose", id = "6d883fe7-5fc0-4a50-8858-72230673eba4") `When I send a GET request`("/workgroups?sort=plop", -REQUEST_PARAM) {
`Given I have citizen`("Alessandro", "Volta", id = "b5bac515-45d4-4aeb-9b6d-2627a0bbc419") } `Then the response should be` BadRequest and {
`Given I have workgroup`("b0ea1922-3bc6-44e2-aa7c-40158998cfbb", createdBy = Name("Blaise", "Pascal")) `And the response should not be null`()
`When I send a POST request`("/workgroups/b0ea1922-3bc6-44e2-aa7c-40158998cfbb/members") { `And the response should contain`("$.invalidParams[0].name", ".sort")
`authenticated as`("Blaise", "Pascal") `And the response should contain`("$.invalidParams[0].reason", "must be one of: 'name', 'createdAt'")
`with body`(
"""
[
{
"citizen": {"id":"6d883fe7-5fc0-4a50-8858-72230673eba4"},
"roles": ["MASTER"]
},
{
"citizen": {"id":"b5bac515-45d4-4aeb-9b6d-2627a0bbc419"},
"roles": ["MASTER"]
}
]
"""
)
} `Then the response should be` Created
}
}
@Test
fun `I can remove member to workgroup`() {
withIntegrationApplication {
`Given I have citizen`("Heinrich", "Hertz", id = "94f92424-c257-4582-907c-98564a8c4ac9")
`Given I have citizen`("William", "Thomson", id = "87909ba3-2069-431c-9924-219fd8411cf2")
`Given I have citizen`("Paul", "Dirac", id = "1baf48bb-02bc-4d8f-ac86-33335354f5e7")
`Given I have workgroup`("b6c975df-dd44-4e99-adc1-f605746b0e11", createdBy = Name("Heinrich", "Hertz")) {
`With members`(
Name("William", "Thomson"),
Name("Paul", "Dirac"),
)
}
`When I send a DELETE request`("/workgroups/b6c975df-dd44-4e99-adc1-f605746b0e11/members") {
`authenticated as`("Heinrich", "Hertz")
"""
[
{
"citizen": {"id":"87909ba3-2069-431c-9924-219fd8411cf2"}
}
]
"""
} `Then the response should be` OK and {
`And the response should contain list`("$", 2)
`And the response should contain`("$.[0]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9")
`And the response should contain`("$.[1]citizen.id", "1baf48bb-02bc-4d8f-ac86-33335354f5e7")
}
}
}
@Test
fun `I can update members on workgroup`() {
withIntegrationApplication {
`Given I have citizen`("Leon", "Foucault")
`Given I have citizen`("Sadi", "Carnot", id = "be3b0926-8628-4426-804a-75188a6eb315")
`Given I have citizen`("Joseph", "Fourier", id = "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1")
`Given I have citizen`("Georg", "Ohm")
`Given I have workgroup`("784fe6bc-7635-4ae2-b080-3a4743b998bf", createdBy = Name("Leon", "Foucault")) {
`With members`(
Name("Sadi", "Carnot"),
Name("Joseph", "Fourier"),
)
}
`When I send a PUT request`("/workgroups/784fe6bc-7635-4ae2-b080-3a4743b998bf/members") {
`authenticated as`("Leon", "Foucault")
`with body`(
"""
[
{
"citizen": {"id":"be3b0926-8628-4426-804a-75188a6eb315"},
"roles": ["MASTER"]
},
{
"citizen": {"id":"b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1"},
"roles": ["MASTER"]
}
]
"""
)
} `Then the response should be` OK and {
`And the response should contain list`("$", 2)
`And the response should contain`("$.[0]citizen.id", "be3b0926-8628-4426-804a-75188a6eb315")
`And the response should contain`("$.[1]citizen.id", "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1")
} }
} }
} }

View File

@@ -6,6 +6,7 @@ import fr.dcproject.component.article.database.ArticleForUpdate
import fr.dcproject.component.article.database.ArticleForView 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.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.workgroup.database.WorkgroupRef import fr.dcproject.component.workgroup.database.WorkgroupRef
import io.ktor.server.testing.TestApplicationEngine import io.ktor.server.testing.TestApplicationEngine
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
@@ -16,7 +17,15 @@ fun TestApplicationEngine.`Given I have article`(
workgroup: WorkgroupRef? = null, workgroup: WorkgroupRef? = null,
createdBy: Name? = null createdBy: Name? = null
) { ) {
createArticle(id?.toUUID(), workgroup, createdBy) createArticle(id?.toUUID(), workgroup, createCitizen(name = createdBy))
}
fun TestApplicationEngine.`Given I have article`(
id: String? = null,
workgroup: WorkgroupRef? = null,
createdBy: UUID
) {
createArticle(id?.toUUID(), workgroup, createCitizen(id = createdBy))
} }
fun TestApplicationEngine.`Given I have articles`( fun TestApplicationEngine.`Given I have articles`(
@@ -35,18 +44,16 @@ fun TestApplicationEngine.`Given I have article created by workgroup`(
fun createArticle( fun createArticle(
id: UUID? = null, id: UUID? = null,
workgroup: WorkgroupRef? = null, workgroup: WorkgroupRef? = null,
createdBy: Name? = null createdBy: CitizenRef = createCitizen()
): ArticleForView { ): ArticleForView {
val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() } val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() }
val citizen = createCitizen(createdBy)
val article = ArticleForUpdate( val article = ArticleForUpdate(
id = id ?: UUID.randomUUID(), id = id ?: UUID.randomUUID(),
title = LoremIpsum().getTitle(3), title = LoremIpsum().getTitle(3),
content = LoremIpsum().getParagraphs(1, 2), content = LoremIpsum().getParagraphs(1, 2),
description = LoremIpsum().getParagraphs(1, 2), description = LoremIpsum().getParagraphs(1, 2),
createdBy = citizen, createdBy = createdBy,
workgroup = workgroup, workgroup = workgroup,
versionId = UUID.randomUUID() versionId = UUID.randomUUID()
) )

View File

@@ -3,6 +3,7 @@ package integration.steps.given
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import fr.dcproject.component.auth.jwt.JwtConfig import fr.dcproject.component.auth.jwt.JwtConfig
import fr.dcproject.component.citizen.database.Citizen import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRepository import fr.dcproject.component.citizen.database.CitizenRepository
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.server.testing.TestApplicationRequest import io.ktor.server.testing.TestApplicationRequest
@@ -25,3 +26,23 @@ fun TestApplicationRequest.`authenticated as`(
return citizen return citizen
} }
fun TestApplicationRequest.`authenticated in url as`(
firstName: String,
lastName: String,
): Citizen {
val repo: CitizenRepository by lazy<CitizenRepository> { GlobalContext.get().koin.get() }
val citizen = repo.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist with name $firstName $lastName")
val algorithm = GlobalContext.get().koin.get<JwtConfig>().algorithm
val jwtAsString: String = JWT.create()
.withIssuer("dc-project.fr")
.withClaim("id", citizen.user.id.toString())
.sign(algorithm)
uri += when (uri.contains('?')) {
true -> '&'
false -> '?'
}
uri += "token=$jwtAsString"
return citizen
}

View File

@@ -23,7 +23,7 @@ fun TestApplicationEngine.`Given I have citizen`(
val user = UserForCreate( val user = UserForCreate(
id = id.toUUID(), id = id.toUUID(),
username = "$firstName-$lastName".toLowerCase(), username = "$firstName-$lastName".toLowerCase(),
password = "azerty", password = "Azerty123!",
) )
val citizen = CitizenForCreate( val citizen = CitizenForCreate(
id = id.toUUID(), id = id.toUUID(),
@@ -36,23 +36,24 @@ fun TestApplicationEngine.`Given I have citizen`(
return repo.insertWithUser(citizen)?.also { callback(it) } return repo.insertWithUser(citizen)?.also { callback(it) }
} }
fun createCitizen(createdBy: CitizenI.Name? = null): Citizen { fun createCitizen(name: CitizenI.Name? = null, id: UUID = UUID.randomUUID()): Citizen {
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() } val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
return if (createdBy != null) { return if (name != null) {
citizenRepository.findByName(createdBy) ?: error("Citizen not exist") citizenRepository.findByName(name) ?: error("Citizen not exist")
} else { } else {
val first = "firstName" + UUID.randomUUID().toString() val first = "firstName" + UUID.randomUUID().toString()
val last = "lastName" + UUID.randomUUID().toString() val last = "lastName" + UUID.randomUUID().toString()
val username = ("username" + UUID.randomUUID().toString()) val username = ("username" + UUID.randomUUID().toString())
CitizenForCreate( CitizenForCreate(
id = id,
birthday = DateTime.now(), birthday = DateTime.now(),
name = CitizenI.Name( name = CitizenI.Name(
first, first,
last last
), ),
email = "$first@fakeemail.com", email = "$first@fakeemail.com",
user = UserForCreate(username = username, password = "azerty") user = UserForCreate(username = username, password = "Azerty123!")
).let { ).let {
citizenRepository.insertWithUser(it) ?: error("Unable to create User") citizenRepository.insertWithUser(it) ?: error("Unable to create User")
} }

View File

@@ -2,11 +2,16 @@ package integration.steps.given
import com.thedeanda.lorem.LoremIpsum import com.thedeanda.lorem.LoremIpsum
import fr.dcproject.common.entity.TargetI import fr.dcproject.common.entity.TargetI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.utils.toUUID import fr.dcproject.common.utils.toUUID
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.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import fr.dcproject.component.comment.generic.database.CommentForUpdate import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentForView
import fr.dcproject.component.comment.generic.database.CommentI
import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.constitution.database.ConstitutionRepository import fr.dcproject.component.constitution.database.ConstitutionRepository
@@ -32,14 +37,14 @@ fun TestApplicationEngine.`Given I have comments on article`(
} }
} }
fun createComment( fun <A : ArticleRef> createComment(
id: UUID? = null, id: UUID? = null,
article: ArticleRef? = null, article: A? = null,
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
) { ): CommentForView<TargetRef, CitizenCreator> {
val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() } val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() }
createCommentOnTarget( return createCommentOnTarget(
id, id,
article?.id?.let { articleRepository.findById(article.id) } ?: createArticle(article?.id), article?.id?.let { articleRepository.findById(article.id) } ?: createArticle(article?.id),
createdBy, createdBy,
@@ -56,14 +61,14 @@ fun TestApplicationEngine.`Given I have comment on constitution`(
createComment(id?.toUUID(), ConstitutionRef(constitution?.toUUID()), createdBy, content) createComment(id?.toUUID(), ConstitutionRef(constitution?.toUUID()), createdBy, content)
} }
fun createComment( fun <C : ConstitutionRef> createComment(
id: UUID? = null, id: UUID? = null,
constitution: ConstitutionRef? = null, constitution: C? = null,
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
) { ): CommentForView<TargetRef, CitizenCreator> {
val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().koin.get() } val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().koin.get() }
createCommentOnTarget( return createCommentOnTarget(
id, id,
constitution?.id?.let { constitutionRepository.findById(constitution.id) } ?: createConstitution(constitution?.id), constitution?.id?.let { constitutionRepository.findById(constitution.id) } ?: createConstitution(constitution?.id),
createdBy, createdBy,
@@ -71,12 +76,12 @@ fun createComment(
) )
} }
fun createCommentOnTarget( fun <T : TargetI> createCommentOnTarget(
id: UUID? = null, id: UUID? = null,
target: TargetI, target: T,
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
) { ): CommentForView<TargetRef, CitizenCreator> {
val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() } val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() }
val creator = createCitizen(createdBy) val creator = createCitizen(createdBy)
val comment = CommentForUpdate( val comment = CommentForUpdate(
@@ -85,5 +90,41 @@ fun createCommentOnTarget(
target = target, target = target,
content = content ?: LoremIpsum().getParagraphs(1, 3) content = content ?: LoremIpsum().getParagraphs(1, 3)
) )
commentRepository.comment(comment) return commentRepository.comment(comment)
}
fun TestApplicationEngine.`Given I have comment on comment`(
id: String? = null,
parent: String? = null,
createdBy: Name? = null,
content: String? = null,
): CommentForView<out TargetRef, CitizenCreator> {
return createCommentOnComment(
id?.toUUID() ?: UUID.randomUUID(),
parent?.run { CommentRef(toUUID()) },
createdBy,
content,
)
}
fun createCommentOnComment(
id: UUID? = null,
parent: CommentI? = createComment<ArticleRef>(),
createdBy: Name? = null,
content: String? = null
): CommentForView<out TargetRef, CitizenCreator> {
val creator = createCitizen(createdBy)
val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() }
val parentComment = if (parent == null) {
createComment<ArticleRef>()
} else {
commentRepository.findById(parent.id) ?: error("Parent of comment not found")
}
val comment = CommentForUpdate(
id = id ?: UUID.randomUUID(),
createdBy = creator,
content = content ?: LoremIpsum().getParagraphs(1, 3),
parent = parentComment,
)
return commentRepository.comment(comment)
} }

View File

@@ -3,6 +3,7 @@ package integration.steps.given
import fr.dcproject.common.utils.toUUID import fr.dcproject.common.utils.toUUID
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.citizen.database.Citizen import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.citizen.database.CitizenRepository import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
@@ -30,7 +31,7 @@ fun TestApplicationEngine.`Given I have follow on article`(
article: String, article: String,
) { ) {
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() } val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
val citizen = citizenRepository.findByUsername("$firstName-$lastName".toLowerCase()) ?: error("Citizen not exist") val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist")
createFollow(citizen, ArticleRef(article.toUUID())) createFollow(citizen, ArticleRef(article.toUUID()))
} }
@@ -40,7 +41,7 @@ fun TestApplicationEngine.`Given I have follow on constitution`(
constitution: String, constitution: String,
) { ) {
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() } val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
val citizen = citizenRepository.findByUsername("$firstName-$lastName".toLowerCase()) ?: error("Citizen not exist") val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist")
createFollow(citizen, ArticleRef(constitution.toUUID())) createFollow(citizen, ArticleRef(constitution.toUUID()))
} }

View File

@@ -65,7 +65,7 @@ private fun createWorkgroup(
.toLowerCase().replace(' ', '-') .toLowerCase().replace(' ', '-')
val user = UserForCreate( val user = UserForCreate(
username = username, username = username,
password = "azerty", password = "Azerty123!",
) )
CitizenForCreate( CitizenForCreate(
name = creatorName, name = creatorName,

View File

@@ -46,7 +46,7 @@ infix fun TestApplicationResponse.`And have property`(path: String): Pair<JsonPa
} ?: throw AssertionError("\"${path}\" element not found on json response") } ?: throw AssertionError("\"${path}\" element not found on json response")
} }
infix fun Pair<JsonPath, Any>.`whish contains`(expected: Any): Pair<JsonPath, Any> = this.apply { infix fun Pair<JsonPath, Any>.`which contains`(expected: Any): Pair<JsonPath, Any> = this.apply {
second `should be equal to` expected second `should be equal to` expected
} }

View File

@@ -1,5 +1,6 @@
package integration.steps.`when` package integration.steps.`when`
import fr.dcproject.common.BitMask
import fr.dcproject.common.BitMaskI import fr.dcproject.common.BitMaskI
import integration.steps.then.`And the schema parameters must be valid` import integration.steps.then.`And the schema parameters must be valid`
import integration.steps.then.`And the schema request body must be valid` import integration.steps.then.`And the schema request body must be valid`
@@ -14,6 +15,7 @@ import io.ktor.server.testing.TestApplicationRequest
import io.ktor.server.testing.setBody import io.ktor.server.testing.setBody
enum class Validate(override val bit: Long) : BitMaskI { enum class Validate(override val bit: Long) : BitMaskI {
NONE(0),
REQUEST_BODY(1), REQUEST_BODY(1),
REQUEST_PARAM(2), REQUEST_PARAM(2),
REQUEST_HEADER(4), REQUEST_HEADER(4),
@@ -22,6 +24,8 @@ enum class Validate(override val bit: Long) : BitMaskI {
RESPONSE_HEADER(16), RESPONSE_HEADER(16),
RESPONSE(8 + 16), RESPONSE(8 + 16),
ALL((1 + 2 + 4) + (8 + 16)); ALL((1 + 2 + 4) + (8 + 16));
operator fun unaryMinus(): BitMaskI = ALL - BitMask(this.bit)
} }
fun TestApplicationCall.valid(validate: BitMaskI): TestApplicationCall { fun TestApplicationCall.valid(validate: BitMaskI): TestApplicationCall {
@@ -40,7 +44,7 @@ fun TestApplicationCall.valid(validate: BitMaskI): TestApplicationCall {
return this return this
} }
fun TestApplicationEngine.`When I send a GET request`(uri: String? = null, validate: Validate = Validate.ALL, setup: (TestApplicationRequest.() -> Unit)? = null): TestApplicationCall { fun TestApplicationEngine.`When I send a GET request`(uri: String? = null, validate: BitMaskI = Validate.ALL, setup: (TestApplicationRequest.() -> Unit)? = null): TestApplicationCall {
return handleRequest(true) { return handleRequest(true) {
method = HttpMethod.Get method = HttpMethod.Get
if (uri != null) { if (uri != null) {
@@ -74,7 +78,7 @@ fun TestApplicationEngine.`When I send a PUT request`(uri: String? = null, valid
}.valid(validate) }.valid(validate)
} }
fun TestApplicationEngine.`When I send a DELETE request`(uri: String? = null, validate: Validate = Validate.ALL, setup: (TestApplicationRequest.() -> String?)? = null): TestApplicationCall { fun TestApplicationEngine.`When I send a DELETE request`(uri: String? = null, validate: BitMaskI = Validate.ALL, setup: (TestApplicationRequest.() -> String?)? = null): TestApplicationCall {
return handleRequest(true) { return handleRequest(true) {
method = HttpMethod.Delete method = HttpMethod.Delete
if (uri != null) { if (uri != null) {

View File

@@ -0,0 +1,32 @@
package unit
import fr.dcproject.common.validation.email
import io.konform.validation.Invalid
import io.konform.validation.Valid
import io.konform.validation.Validation
import org.amshove.kluent.`should be instance of`
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(ExecutionMode.CONCURRENT)
@Tags(Tag("validation"), Tag("unit"))
internal class `Email Validation` {
@Test
fun passwordScore() {
Validation<ObjectToValid> {
ObjectToValid::email {
email()
}
}.run {
validate(ObjectToValid("abc@123.com")) `should be instance of` Valid::class
validate(ObjectToValid("abc123.com")) `should be instance of` Invalid::class
}
}
class ObjectToValid(val email: String)
}

View File

@@ -0,0 +1,46 @@
package unit
import fr.dcproject.common.validation.passwordScore
import io.konform.validation.Invalid
import io.konform.validation.Valid
import io.konform.validation.Validation
import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should be instance of`
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(ExecutionMode.CONCURRENT)
@Tags(Tag("validation"), Tag("unit"))
internal class `Password Validation` {
@Test
fun password() {
"1234567890".passwordScore() `should be equal to` 10
"1234567A".passwordScore() `should be equal to` 10
"1234Aa".passwordScore() `should be equal to` 10
"12Aab".passwordScore() `should be equal to` 11
"1234Aa".passwordScore() `should be equal to` 10
"12abCD-+".passwordScore() `should be equal to` 18
"Abcde12!".passwordScore() `should be equal to` 15
"Hello world".passwordScore() `should be equal to` 16
"hello WORLD".passwordScore() `should be equal to` 17
}
@Test
fun passwordScore() {
Validation<ObjectToValid> {
ObjectToValid::password {
this.passwordScore(10)
}
}.run {
validate(ObjectToValid("1234567890")) `should be instance of` Valid::class
validate(ObjectToValid("12345678")) `should be instance of` Invalid::class
}
}
class ObjectToValid(val password: String)
}

View File

@@ -22,7 +22,7 @@ begin
select "comment"( select "comment"(
reference => 'article'::regclass, reference => 'article'::regclass,
resource => _comment resource => _comment
) into _comment_id; )->>'id' into _comment_id;
assert (select count(*) = 1 from "comment"), 'comment must be inserted, "' || (select count(*) from "comment") || '" exist'; assert (select count(*) = 1 from "comment"), 'comment must be inserted, "' || (select count(*) from "comment") || '" exist';
assert (select com.content = 'Ho my god !' from "comment" com), 'the content of comment must be "Ho my god !" instead of "' || (select com.content from "comment" as com) || '"'; assert (select com.content = 'Ho my god !' from "comment" com), 'the content of comment must be "Ho my god !" instead of "' || (select com.content from "comment" as com) || '"';
@@ -67,7 +67,7 @@ begin
select "comment"( select "comment"(
reference => 'article'::regclass, reference => 'article'::regclass,
resource => _comment resource => _comment
) into _comment_id_response; )->>'id' into _comment_id_response;
_comment = json_build_object( _comment = json_build_object(
@@ -80,7 +80,7 @@ begin
select "comment"( select "comment"(
reference => 'article'::regclass, reference => 'article'::regclass,
resource => _comment resource => _comment
) into _comment_id_response2; )->>'id' into _comment_id_response2;
assert (select count(*) = 3 from "comment"), 'response must be inserted'; assert (select count(*) = 3 from "comment"), 'response must be inserted';
assert (select com.parents_ids @> ARRAY[_comment_id] from "comment" com where id = _comment_id_response), 'parents_ids not contain "' || _comment_id::text || '" ' || (select com.parents_ids::text[] from "comment" com where id = _comment_id_response); assert (select com.parents_ids @> ARRAY[_comment_id] from "comment" com where id = _comment_id_response), 'parents_ids not contain "' || _comment_id::text || '" ' || (select com.parents_ids::text[] from "comment" com where id = _comment_id_response);
assert (select com.parents_ids @> ARRAY[_comment_id_response] from "comment" com where id = _comment_id_response2), 'parents_ids not contain "' || _comment_id_response::text || '" ' || (select com.parents_ids::text[] from "comment" com where id = _comment_id_response2); assert (select com.parents_ids @> ARRAY[_comment_id_response] from "comment" com where id = _comment_id_response2), 'parents_ids not contain "' || _comment_id_response::text || '" ' || (select com.parents_ids::text[] from "comment" com where id = _comment_id_response2);

View File

@@ -29,6 +29,9 @@ begin
assert (select following = true from find_follow(first_article_id, _citizen_id, 'article')), '(v1) find_follow must return the following'; assert (select following = true from find_follow(first_article_id, _citizen_id, 'article')), '(v1) find_follow must return the following';
assert (select following = true from find_follow(first_article_updated_id, _citizen_id, 'article')), '(v2) find_follow must return the following'; assert (select following = true from find_follow(first_article_updated_id, _citizen_id, 'article')), '(v2) find_follow must return the following';
assert (select f.total = 1 from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return 1 follow';
assert (select (f.resource#>>'{0, created_by, id}')::uuid = _citizen_id from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return follows with creator';
perform unfollow('article'::regclass, first_article_id, _citizen_id); perform unfollow('article'::regclass, first_article_id, _citizen_id);
assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow, event if article is on other version'; assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow, event if article is on other version';