1 Commits

Author SHA1 Message Date
32f62ec5ab Add validation to notification 2021-04-14 23:53:19 +02:00
109 changed files with 764 additions and 2435 deletions

View File

@@ -4,9 +4,6 @@
name: Tests name: Tests
on: on:
push:
branches:
- 'master'
pull_request: pull_request:
branches: branches:
- 'master' - 'master'
@@ -21,10 +18,6 @@ jobs:
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 11
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
- name: Cache Gradle packages - name: Cache Gradle packages
uses: actions/cache@v2 uses: actions/cache@v2
@@ -36,17 +29,26 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-gradle- ${{ runner.os }}-gradle-
- name: Build - name: Build
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt arguments: build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x detekt
- name: Cleanup Gradle Cache
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties
- name: processResources - name: processResources
run: gradle processResources uses: eskatos/gradle-command-action@v1
- name: processTestResources
uses: gradle/gradle-build-action@v2
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: processResources
- name: processTestResources
uses: eskatos/gradle-command-action@v1
with:
gradle-version: 6.8
arguments: processResources arguments: processResources
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
@@ -67,17 +69,10 @@ jobs:
with: with:
name: Build name: Build
path: build path: build
- name: Composer Up
uses: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testSqlComposeUp
- name: TestSql - name: TestSql
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: testSql arguments: testSql
test: test:
@@ -86,58 +81,37 @@ 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: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: testComposeUp
- name: Test - name: Test
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: test arguments: test -x testSql
- name: Coverage - name: Coverage
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
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: gradle/gradle-build-action@v2
with:
gradle-version: '7.4'
arguments: test
- name: Build and analyze - name: Build and analyze
uses: gradle/gradle-build-action@v2
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 }}
with: run: ./gradlew build sonarqube --info
gradle-version: '7.4'
arguments: sonarqube --info
lint: lint:
needs: build needs: build
@@ -154,7 +128,7 @@ jobs:
name: Build name: Build
path: build path: build
- name: Lint - name: Lint
uses: gradle/gradle-build-action@v2 uses: eskatos/gradle-command-action@v1
with: with:
gradle-version: '7.4' gradle-version: 6.8
arguments: ktlintCheck arguments: ktlintCheck

View File

@@ -8,7 +8,7 @@
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<SqlCodeStyleSettings version="6"> <SqlCodeStyleSettings version="5">
<option name="KEYWORD_CASE" value="1" /> <option name="KEYWORD_CASE" value="1" />
<option name="IDENTIFIER_CASE" value="1" /> <option name="IDENTIFIER_CASE" value="1" />
<option name="TYPE_CASE" value="4" /> <option name="TYPE_CASE" value="4" />
@@ -56,13 +56,21 @@
</indentOptions> </indentOptions>
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="kotlin"> <codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" /> <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" /> <option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" /> <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" /> <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="5" />
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions> </indentOptions>

View File

@@ -25,9 +25,12 @@
<option value="openapi" /> <option value="openapi" />
<option value="rabbitmq" /> <option value="rabbitmq" />
<option value="redis" /> <option value="redis" />
<option value="sonarqube" />
</list> </list>
</option> </option>
<option name="sourceFilePath" value="docker-compose.yml" /> <option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings> </settings>
</deployment> </deployment>
<method v="2" /> <method v="2" />

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 -x detekt" /> <option name="scriptParameters" value="-x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck" />
<option name="taskDescriptions"> <option name="taskDescriptions">
<list /> <list />
</option> </option>

View File

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

View File

@@ -24,9 +24,13 @@
<option value="rabbitmq" /> <option value="rabbitmq" />
<option value="redis" /> <option value="redis" />
<option value="openapi" /> <option value="openapi" />
<option value="sonarqube" />
<option value="sonarqube_db" />
</list> </list>
</option> </option>
<option name="sourceFilePath" value="docker-compose.yml" /> <option name="sourceFilePath" value="docker-compose.yml" />
<option name="upExitCodeFromService" value="" />
<option name="upTimeout" value="" />
</settings> </settings>
</deployment> </deployment>
<method v="2" /> <method v="2" />

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube (Send without run test)" type="GradleRunConfiguration" factoryName="Gradle"> <configuration default="false" name="Sonarqube without 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

@@ -1,23 +0,0 @@
<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,23 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Update Dependency" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--update-locks *:*" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="classes" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -9,12 +9,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.owasp.dependencycheck.reporting.ReportGenerator import org.owasp.dependencycheck.reporting.ReportGenerator
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
val ktorVersion = "1.5.4" val ktorVersion = "1.5.0"
val kotlinVersion = "1.5.31" val kotlinVersion = "1.4.30"
val coroutinesVersion = "1.5.2" val coroutinesVersion = "1.4.3"
val logbackVersion = "1.2.3" val logbackVersion = "1.2.3"
val koinVersion = "3.1.5" val koinVersion = "2.0.1"
val jacksonVersion = "2.13.1" val jacksonVersion = "2.12.1"
group = "com.github.flecomte" group = "com.github.flecomte"
version = versioning.info.run { version = versioning.info.run {
@@ -28,24 +28,20 @@ version = versioning.info.run {
plugins { plugins {
jacoco jacoco
application application
`maven-publish` maven
kotlin("jvm") version "1.5.31" id("maven-publish")
kotlin("plugin.serialization") version "1.5.31" kotlin("jvm") version "1.4.30"
kotlin("plugin.serialization") version "1.4.30"
id("com.github.johnrengelman.shadow") version "7.1.2" id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.jlleitschuh.gradle.ktlint") version "10.2.1" id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
id("org.owasp.dependencycheck") version "6.1.5" id("org.owasp.dependencycheck") version "6.1.1"
id("org.sonarqube") version "3.3" id("org.sonarqube") version "3.1.1"
id("net.nemerosa.versioning") version "2.15.1" id("net.nemerosa.versioning") version "2.14.0"
id("io.gitlab.arturbosch.detekt") version "1.19.0" id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
id("com.avast.gradle.docker-compose") version "0.15.1" id("com.avast.gradle.docker-compose") version "0.14.0"
id("com.github.kt3k.coveralls") version "2.12.0" id("com.github.kt3k.coveralls") version "2.8.4"
}
dependencyLocking {
lockAllConfigurations()
// lockMode.set(LockMode.STRICT)
} }
application { application {
@@ -60,7 +56,7 @@ buildscript {
maven { url = uri("https://jitpack.io") } maven { url = uri("https://jitpack.io") }
} }
dependencies { dependencies {
classpath("com.typesafe:config:1.4.2") classpath("com.typesafe:config:1.4.1")
classpath("com.github.flecomte:postgres-json:2.1.2") classpath("com.github.flecomte:postgres-json:2.1.2")
} }
} }
@@ -98,7 +94,7 @@ val migration by tasks.registering {
} }
val migrationTest by tasks.registering { val migrationTest by tasks.registering {
group = "tests" group = "verification"
dependsOn(tasks.named("testComposeUp")) dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown")) finalizedBy(tasks.named("testComposeDown"))
doLast { doLast {
@@ -122,9 +118,11 @@ val migrationTest by tasks.registering {
} }
val testSql by tasks.registering { val testSql by tasks.registering {
group = "tests" group = "verification"
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()
@@ -169,7 +167,6 @@ tasks.withType<Jar> {
) )
) )
} }
isZip64 = true
} }
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
@@ -183,10 +180,11 @@ tasks.withType<KotlinCompile> {
tasks.named<ShadowJar>("shadowJar") { tasks.named<ShadowJar>("shadowJar") {
mergeServiceFiles("META-INF/services") mergeServiceFiles("META-INF/services")
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}") archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}")
isZip64 = true
} }
tasks.sonarqube.configure { tasks.sonarqube.configure {
dependsOn(tasks.test)
dependsOn(tasks.detekt)
dependsOn(tasks.jacocoTestReport) dependsOn(tasks.jacocoTestReport)
} }
@@ -199,7 +197,8 @@ val sourcesJar by tasks.registering(Jar::class) {
tasks.test { 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
} }
@@ -207,6 +206,13 @@ 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"
@@ -222,12 +228,14 @@ 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)
} }
} }
@@ -258,7 +266,7 @@ publishing {
} }
jacoco { jacoco {
toolVersion = "0.8.7" toolVersion = "0.8.6"
applyTo(tasks.run.get()) applyTo(tasks.run.get())
} }
@@ -312,14 +320,6 @@ tasks.named("testComposeUp").configure {
} }
} }
tasks.register("testWithDependencies", Test::class) {
group = "tests"
dependsOn(tasks.named("testComposeUp"))
dependsOn(tasks.ktlintCheck)
dependsOn(testSql)
dependsOn(tasks.jacocoTestReport)
finalizedBy(tasks.sonarqube) // report is always generated after tests run
}
tasks.register("testArticles", Test::class) { tasks.register("testArticles", Test::class) {
group = "tests" group = "tests"
useJUnitPlatform { useJUnitPlatform {
@@ -356,30 +356,6 @@ tasks.register("testNotifications", Test::class) {
includeTags("notification") 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)
@@ -388,7 +364,9 @@ dependencyCheck {
repositories { repositories {
mavenLocal() mavenLocal()
jcenter() jcenter()
maven { url = uri("https://jitpack.io") } maven("https://kotlin.bintray.com/ktor")
maven("https://jitpack.io")
maven("https://dl.bintray.com/konform-kt/konform")
} }
dependencies { dependencies {
@@ -405,7 +383,7 @@ dependencies {
implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktorVersion") implementation("io.ktor:ktor-auth-jwt:$ktorVersion")
implementation("io.ktor:ktor-websockets:$ktorVersion") implementation("io.ktor:ktor-websockets:$ktorVersion")
implementation("io.insert-koin:koin-ktor:$koinVersion") implementation("org.koin:koin-ktor:$koinVersion")
implementation("io.ktor:ktor-jackson:$ktorVersion") implementation("io.ktor:ktor-jackson:$ktorVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion") implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion")
@@ -419,18 +397,18 @@ 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:0.3.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")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion") testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion")
testImplementation("io.insert-koin:koin-test:$koinVersion") testImplementation("org.koin:koin-test:$koinVersion")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.amshove.kluent:kluent:1.68") testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
testImplementation("io.mockk:mockk:1.12.2") testImplementation("org.amshove.kluent:kluent:1.61")
testImplementation("io.mockk:mockk-agent-api:1.12.2") testImplementation("io.mockk:mockk-agent-api:1.10.6")
testImplementation("io.mockk:mockk-agent-jvm:1.12.2") testImplementation("io.mockk:mockk-agent-jvm:1.10.6")
testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") testImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
testImplementation("com.thedeanda:lorem:2.1") testImplementation("com.thedeanda:lorem:2.1")
testImplementation("org.openapi4j:openapi-operation-validator:1.0.6") testImplementation("org.openapi4j:openapi-operation-validator:1.0.6")

View File

@@ -1,4 +1,4 @@
version: '3.3' version: '3.8'
services: services:
rabbitmq: rabbitmq:
container_name: ${APP_NAME}_rabbitmq_test container_name: ${APP_NAME}_rabbitmq_test

View File

@@ -1,275 +0,0 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
ch.qos.logback:logback-classic:1.2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
ch.qos.logback:logback-core:1.2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.auth0:java-jwt:3.12.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.auth0:jwks-rsa:0.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.avast.gradle:gradle-docker-compose-plugin:0.14.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.beust:jcommander:1.81=detekt
com.fasterxml.jackson.core:jackson-annotations:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-joda:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.13.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.github.flecomte:postgres-json:2.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.github.jasync-sql:jasync-common:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-common:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.jasync-sql:jasync-pool:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-pool:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.jasync-sql:jasync-postgresql:1.1.6=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
com.github.jasync-sql:jasync-postgresql:1.1.7=runtimeClasspath,testRuntimeClasspath
com.github.shyiko.klob:klob:0.2.1=ktlint
com.google.code.findbugs:jsr305:3.0.2=runtimeClasspath,testRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.2.0=runtimeClasspath,testRuntimeClasspath
com.google.guava:failureaccess:1.0.1=runtimeClasspath,testRuntimeClasspath
com.google.guava:guava:27.1-jre=runtimeClasspath,testRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=runtimeClasspath,testRuntimeClasspath
com.google.j2objc:j2objc-annotations:1.1=runtimeClasspath,testRuntimeClasspath
com.googlecode.json-simple:json-simple:1.1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.jayway.jsonpath:json-path:2.5.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.scram:client:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.scram:common:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.stringprep:saslprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.ongres.stringprep:stringprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.pinterest.ktlint:ktlint-core:0.42.1=ktlint,ktlintBaselineReporter
com.pinterest.ktlint:ktlint-reporter-baseline:0.42.1=ktlint,ktlintBaselineReporter
com.pinterest.ktlint:ktlint-reporter-checkstyle:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-html:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-json:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-plain:0.42.1=ktlint
com.pinterest.ktlint:ktlint-reporter-sarif:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-experimental:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-standard:0.42.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-test:0.42.1=ktlint
com.pinterest:ktlint:0.42.1=ktlint
com.rabbitmq:amqp-client:5.10.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.sendgrid:java-http-client:4.3.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.sendgrid:sendgrid-java:4.7.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.thedeanda:lorem:2.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.typesafe:config:1.3.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
commons-codec:commons-codec:1.11=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
commons-codec:commons-codec:1.14=runtimeClasspath,testRuntimeClasspath
commons-io:commons-io:2.6=runtimeClasspath,testRuntimeClasspath
commons-logging:commons-logging:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
info.picocli:picocli:3.9.6=ktlint
io.github.detekt.sarif4k:sarif4k:0.0.1=detekt,ktlint
io.github.microutils:kotlin-logging:1.7.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.gitlab.arturbosch.detekt:detekt-api:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-cli:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-core:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-metrics:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-parser:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-psi-utils:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-html:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-sarif:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-txt:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-report-xml:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-complexity:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-coroutines:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-documentation:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-empty:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-errorprone:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-exceptions:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-naming:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-performance:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules-style:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-rules:1.19.0=detekt
io.gitlab.arturbosch.detekt:detekt-tooling:1.19.0=detekt
io.insert-koin:koin-core-jvm:3.1.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.insert-koin:koin-core:3.1.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.insert-koin:koin-ktor:3.1.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.insert-koin:koin-test-jvm:3.1.5=testCompileClasspath,testRuntimeClasspath
io.insert-koin:koin-test:3.1.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.konform:konform-jvm:0.3.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.konform:konform:0.3.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth-jwt-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-auth-jwt:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth-kotlinMultiplatform:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-auth:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-cio-jvm:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-cio:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-core-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-client-core:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-jetty-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-jetty:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-mock-jvm:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-client-mock:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-http-cio-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-http-cio:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-http-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-http:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-io-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-io:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-jackson-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-jackson:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-locations-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-locations:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-network-jvm:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-certificates-kotlinMultiplatform:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-certificates:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls-jvm:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network-tls:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-network:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-core-kotlinMultiplatform:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-core:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-host-common-kotlinMultiplatform:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-host-common:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-jetty-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testImplementationDependenciesMetadata
io.ktor:ktor-server-jetty:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-server-servlet-kotlinMultiplatform:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-servlet:1.5.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-test-host-kotlinMultiplatform:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-test-host:1.5.4=testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-server-tests-kotlinMultiplatform:1.5.4=testImplementationDependenciesMetadata
io.ktor:ktor-server-tests:1.5.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-utils-jvm:1.6.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.ktor:ktor-utils:1.6.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-websockets-kotlinMultiplatform:1.5.4=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.ktor:ktor-websockets:1.5.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.lettuce:lettuce-core:5.3.6.RELEASE=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-api:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-common:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-agent-jvm:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-common:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-dsl-jvm:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk-dsl:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.mockk:mockk:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-buffer:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-codec:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-common:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-handler:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-resolver:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.netty:netty-transport:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.projectreactor:reactor-core:3.4.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
javax.servlet:javax.servlet-api:3.1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
joda-time:joda-time:2.10.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
junit:junit:4.12=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.12.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.12.5=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.java.dev.jna:jna-platform:5.5.0=testRuntimeClasspath
net.java.dev.jna:jna:5.5.0=testRuntimeClasspath
net.minidev:accessors-smart:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.minidev:json-smart:2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.pearx.kasechange:kasechange-jvm:1.3.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.pearx.kasechange:kasechange-metadata:1.3.0=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.amshove.kluent:kluent-common:1.68=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.amshove.kluent:kluent:1.68=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpasyncclient:4.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpclient:4.5.12=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpcore-nio:4.4.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apache.httpcomponents:httpcore:4.4.13=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata
org.bouncycastle:bcprov-jdk15on:1.67=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.checkerframework:checker-qual:2.5.2=runtimeClasspath,testRuntimeClasspath
org.codehaus.mojo:animal-sniffer-annotations:1.17=runtimeClasspath,testRuntimeClasspath
org.ec4j.core:ec4j-core:0.3.0=ktlint,ktlintBaselineReporter
org.eclipse.jetty.http2:http2-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-common:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-hpack:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-http-client-transport:9.4.31.v20200723=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.http2:http2-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-java-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-java-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-openjdk8-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-openjdk8-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-client:9.4.31.v20200723=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-continuation:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-http:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-io:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-servlets:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.elasticsearch.client:elasticsearch-rest-client:6.7.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.fusesource.jansi:jansi:2.3.4=runtimeClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath
org.jacoco:org.jacoco.agent:0.8.7=jacocoAgent,jacocoAnt
org.jacoco:org.jacoco.ant:0.8.7=jacocoAnt
org.jacoco:org.jacoco.core:0.8.7=jacocoAnt
org.jacoco:org.jacoco.report:0.8.7=jacocoAnt
org.jetbrains.intellij.deps:trove4j:1.0.20181211=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.5.31=kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-native-utils:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-project-model:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-reflect:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-reflect:1.5.30=compileClasspath,implementationDependenciesMetadata,runtimeClasspath
org.jetbrains.kotlin:kotlin-reflect:1.5.31=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-script-runtime:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-script-runtime:1.5.31=detekt,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-scripting-common:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-jvm:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-serialization:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.5.20=ktlint,ktlintBaselineReporter
org.jetbrains.kotlin:kotlin-stdlib:1.5.31=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-annotations-common:1.5.31=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-common:1.5.31=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-junit:1.5.31=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test:1.5.31=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-util-io:1.5.31=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.2=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-metadata:1.4.2=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.5.2=testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.1-native-mt=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.5.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.3=detekt
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-core-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.1.0=detekt,ktlint
org.jetbrains.kotlinx:kotlinx-serialization-json-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0=detekt,ktlint
org.jetbrains:annotations:13.0=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.joda:joda-convert:1.8.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.8.2=testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.8.2=testRuntimeClasspath
org.junit:junit-bom:5.8.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.objenesis:objenesis:3.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-core:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-operation-validator:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-parser:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.openapi4j:openapi-schema-validator:1.0.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.1=jacocoAnt
org.ow2.asm:asm-commons:9.1=jacocoAnt
org.ow2.asm:asm-tree:9.1=jacocoAnt
org.ow2.asm:asm:5.0.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.ow2.asm:asm:9.1=jacocoAnt
org.reactivestreams:reactive-streams:1.0.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.slf4j:slf4j-api:1.7.30=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.yaml:snakeyaml:1.27=runtimeClasspath
org.yaml:snakeyaml:1.28=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.yaml:snakeyaml:1.29=detekt
empty=annotationProcessor,apiDependenciesMetadata,compileOnly,compileOnlyDependenciesMetadata,detektPlugins,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,ktlintReporter,ktlintRuleset,runtimeOnlyDependenciesMetadata,shadow,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntimeOnlyDependenciesMetadata

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -9,7 +9,6 @@ import com.fasterxml.jackson.datatype.joda.JodaModule
import fr.dcproject.application.Env.PROD import fr.dcproject.application.Env.PROD
import fr.dcproject.application.Env.TEST import fr.dcproject.application.Env.TEST
import fr.dcproject.application.http.statusPagesInstallation import fr.dcproject.application.http.statusPagesInstallation
import fr.dcproject.common.utils.onApplicationStopped
import fr.dcproject.component.article.articleKoinModule import fr.dcproject.component.article.articleKoinModule
import fr.dcproject.component.article.routes.installArticleRoutes import fr.dcproject.component.article.routes.installArticleRoutes
import fr.dcproject.component.auth.authKoinModule import fr.dcproject.component.auth.authKoinModule
@@ -26,10 +25,8 @@ import fr.dcproject.component.constitution.routes.installConstitutionRoutes
import fr.dcproject.component.doc.routes.installDocRoutes import fr.dcproject.component.doc.routes.installDocRoutes
import fr.dcproject.component.follow.followKoinModule import fr.dcproject.component.follow.followKoinModule
import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes
import fr.dcproject.component.follow.routes.citizen.installFollowCitizenRoutes
import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes
import fr.dcproject.component.notification.email.NotificationEmailConsumer import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.push.NotificationPushConsumer
import fr.dcproject.component.notification.routes.installNotificationsRoutes import fr.dcproject.component.notification.routes.installNotificationsRoutes
import fr.dcproject.component.opinion.opinionKoinModule import fr.dcproject.component.opinion.opinionKoinModule
import fr.dcproject.component.opinion.routes.installOpinionRoutes import fr.dcproject.component.opinion.routes.installOpinionRoutes
@@ -40,6 +37,7 @@ import fr.dcproject.component.workgroup.routes.installWorkgroupRoutes
import fr.dcproject.component.workgroup.workgroupKoinModule import fr.dcproject.component.workgroup.workgroupKoinModule
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
import io.ktor.application.install import io.ktor.application.install
import io.ktor.auth.Authentication import io.ktor.auth.Authentication
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
@@ -59,6 +57,7 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations import io.ktor.locations.Locations
import io.ktor.routing.Routing import io.ktor.routing.Routing
import io.ktor.server.jetty.EngineMain import io.ktor.server.jetty.EngineMain
import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog import org.eclipse.jetty.util.log.Slf4jLog
@@ -73,6 +72,7 @@ fun main(args: Array<String>): Unit = EngineMain.main(args)
enum class Env { PROD, TEST } enum class Env { PROD, TEST }
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf @Suppress("unused") // Referenced in application.conf
fun Application.module(env: Env = PROD) { fun Application.module(env: Env = PROD) {
@@ -117,14 +117,11 @@ fun Application.module(env: Env = PROD) {
masking = false masking = false
} }
get<NotificationEmailConsumer>().run { get<NotificationConsumer>().run {
start() start()
onApplicationStopped { close() } environment.monitor.subscribe(ApplicationStopped) {
close()
} }
get<NotificationPushConsumer>().run {
start()
onApplicationStopped { close() }
} }
install(Authentication, jwtInstallation(get(), get())) install(Authentication, jwtInstallation(get(), get()))
@@ -157,7 +154,6 @@ fun Application.module(env: Env = PROD) {
installCommentRoutes() installCommentRoutes()
installFollowArticleRoutes() installFollowArticleRoutes()
installFollowConstitutionRoutes() installFollowConstitutionRoutes()
installFollowCitizenRoutes()
installWorkgroupRoutes() installWorkgroupRoutes()
installOpinionRoutes() installOpinionRoutes()
installVoteRoutes() installVoteRoutes()

View File

@@ -5,10 +5,20 @@ import fr.dcproject.application.http.HttpErrorBadRequest
import fr.dcproject.application.http.HttpErrorBadRequest.InvalidParam 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.http.HttpStatusCode
import io.ktor.util.KtorExperimentalAPI
import org.koin.core.context.GlobalContext
import org.koin.core.parameter.ParametersDefinition
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(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): T = GlobalContext.get().koin.rootScope.get(qualifier, parameters)
@KtorExperimentalAPI
val converters: ConverterDeclaration = { val converters: ConverterDeclaration = {
convert<UUID> { convert<UUID> {
decode { values, _ -> decode { values, _ ->

View File

@@ -10,20 +10,21 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.common.email.Mailer import fr.dcproject.common.email.Mailer
import fr.dcproject.component.auth.jwt.JwtConfig import fr.dcproject.component.auth.jwt.JwtConfig
import fr.dcproject.component.notification.NotificationPublisherAsync import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.email.NotificationEmailConsumer import fr.dcproject.component.notification.NotificationEmailSender
import fr.dcproject.component.notification.email.NotificationEmailSender import fr.dcproject.component.notification.NotificationsPush
import fr.dcproject.component.notification.push.NotificationPushConsumer import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.notification.push.NotificationPushListener
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
@KtorExperimentalAPI
val KoinModule = module { val KoinModule = module {
// JWT // JWT
single { single {
@@ -64,15 +65,11 @@ val KoinModule = module {
} }
} }
single { NotificationPushListener.Builder(get()) } single { NotificationsPush.Builder(get()) }
single { single {
val config: Configuration = get() val config: Configuration = get()
NotificationEmailConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName) NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
}
single {
val config: Configuration = get()
NotificationPushConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
} }
// RabbitMQ // RabbitMQ
@@ -117,7 +114,7 @@ val KoinModule = module {
single { single {
val config: Configuration = get() val config: Configuration = get()
NotificationPublisherAsync(factory = get(), exchangeName = config.exchangeNotificationName) Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
} }
single { single {

View File

@@ -9,9 +9,6 @@ import java.io.IOException
class Mailer( class Mailer(
private val key: String private val key: String
) { ) {
/**
* Send email via Sendgrid
*/
fun sendEmail(action: () -> Mail): Boolean { fun sendEmail(action: () -> Mail): Boolean {
val mail = action() val mail = action()

View File

@@ -2,7 +2,6 @@ package fr.dcproject.common.entity
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.comment.generic.database.CommentRef import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.opinion.database.OpinionRef import fr.dcproject.component.opinion.database.OpinionRef
@@ -35,8 +34,7 @@ interface TargetI : EntityI {
Article("article"), Article("article"),
Constitution("constitution"), Constitution("constitution"),
Comment("comment"), Comment("comment"),
Opinion("opinion"), Opinion("opinion")
Citizen("citizen"),
} }
companion object { companion object {
@@ -46,7 +44,6 @@ interface TargetI : EntityI {
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference
t.isSubclassOf(CitizenRef::class) -> TargetName.Citizen.targetReference
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL") else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
} }
} }

View File

@@ -1,10 +0,0 @@
package fr.dcproject.common.utils
import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
fun Application.onApplicationStopped(callback: Application.() -> Unit) {
environment.monitor.subscribe(ApplicationStopped) {
callback()
}
}

View File

@@ -1,30 +0,0 @@
package fr.dcproject.common.utils
import com.rabbitmq.client.AMQP
import com.rabbitmq.client.Channel
import com.rabbitmq.client.Consumer
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.Envelope
import kotlinx.coroutines.runBlocking
import java.io.IOException
fun Channel.consumeQueue(queueName: String, callback: DefaultConsumer.(ByteArray) -> Unit) {
val consumer: Consumer = object : DefaultConsumer(this) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: AMQP.BasicProperties,
body: ByteArray
) = runBlocking {
try {
callback(body)
basicAck(envelope.deliveryTag, false)
} catch (e: Throwable) {
basicNack(envelope.deliveryTag, false, true)
}
}
}
/* Launch Consumer */
basicConsume(queueName, false, consumer)
}

View File

@@ -1,41 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -37,7 +37,7 @@ class ArticleAccessControl(private val articleRepo: ArticleRepository) : AccessC
if (subject.createdBy.id == citizen.id) { if (subject.createdBy.id == citizen.id) {
/* The creator must be the same of the creator of preview version of article */ /* The creator must be the same of the creator of preview version of article */
val lastVersionId = articleRepo val lastVersionId = articleRepo
.findSiblingVersions(1, 1, subject) .findVersionsByVersionId(1, 1, subject.versionId)
.result .result
.firstOrNull()?.createdBy?.id .firstOrNull()?.createdBy?.id

View File

@@ -87,7 +87,6 @@ class ArticleForListing(
ArticleRef(id), ArticleRef(id),
ArticleAuthI<CitizenCartI>, ArticleAuthI<CitizenCartI>,
Votable by VotableImp(), Votable by VotableImp(),
CreatedAt by CreatedAt.Imp(),
CreatedBy<CitizenCartI> CreatedBy<CitizenCartI>
interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> { interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {

View File

@@ -1,6 +1,5 @@
package fr.dcproject.component.article.database package fr.dcproject.component.article.database
import fr.dcproject.common.entity.VersionableId
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter import fr.postgresjson.entity.Parameter
@@ -20,10 +19,10 @@ class ArticleRepository(override var requester: Requester) : RepositoryI {
.select(page, limit, "id" to id) .select(page, limit, "id" to id)
} }
fun <A> findSiblingVersions(page: Int = 1, limit: Int = 50, article: A): Paginated<ArticleForListing> where A : VersionableId, A : ArticleI { fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForListing> {
return requester return requester
.getFunction("find_articles_versions_by_version_id") .getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to article.versionId) .select(page, limit, "version_id" to versionId)
} }
fun find( fun find(

View File

@@ -83,7 +83,6 @@ object FindArticles {
object { object {
val id = it.id val id = it.id
val title = it.title val title = it.title
val createdAt = it.createdAt
val createdBy: Any = it.createdBy.toOutput() val createdBy: Any = it.createdBy.toOutput()
val workgroup = it.workgroup?.let { val workgroup = it.workgroup?.let {
object { object {

View File

@@ -10,8 +10,8 @@ import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.
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.notification.ArticleUpdateNotificationMessage import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.NotificationPublisherAsync 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.Validation
import io.konform.validation.jsonschema.maxItems import io.konform.validation.jsonschema.maxItems
@@ -63,7 +63,7 @@ object UpsertArticle {
} }
} }
fun Route.upsertArticle(repo: ArticleRepository, notificationPublisher: NotificationPublisherAsync, 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() validate().badRequestIfNotValid()
ArticleForUpdate( ArticleForUpdate(
@@ -92,7 +92,7 @@ object UpsertArticle {
val versionNumber = a.versionNumber val versionNumber = a.versionNumber
} }
) )
notificationPublisher.publishAsync(ArticleUpdateNotificationMessage(a)) publisher.publish(ArticleUpdateNotification(a))
} ?: error("Article not updated") } ?: error("Article not updated")
} }
} }

View File

@@ -17,13 +17,13 @@ private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
val ApplicationCall.citizen: CitizenEntity val ApplicationCall.citizen: CitizenEntity
get() = attributes.computeIfAbsent(citizenAttributeKey) { get() = attributes.computeIfAbsent(citizenAttributeKey) {
val user = authentication.principal<UserI>() ?: throw ForbiddenException("No User Connected") val user = authentication.principal<UserI>() ?: throw ForbiddenException("No User Connected")
GlobalContext.get().get<CitizenRepository>().findByUser(user) GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"") ?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"")
} }
val ApplicationCall.citizenOrNull: CitizenEntity? val ApplicationCall.citizenOrNull: CitizenEntity?
get() = authentication.principal<UserI>()?.let { get() = authentication.principal<UserI>()?.let {
GlobalContext.get().get<CitizenRepository>().findByUser(it) GlobalContext.get().koin.get<CitizenRepository>().findByUser(it)
} }
val ApplicationCall.isAuth: Boolean val ApplicationCall.isAuth: Boolean

View File

@@ -7,7 +7,7 @@ import org.koin.core.context.GlobalContext
/** /**
* Produce a token for this combination of User and Account * Produce a token for this combination of User and Account
*/ */
fun UserI.makeToken(): String = GlobalContext.get().get<JwtConfig>().run { fun UserI.makeToken(): String = GlobalContext.get().koin.get<JwtConfig>().run {
JWT.create() JWT.create()
.withSubject("Authentication") .withSubject("Authentication")
.withIssuer(issuer) .withIssuer(issuer)

View File

@@ -2,9 +2,8 @@ package fr.dcproject.component.citizen.database
import fr.dcproject.common.entity.CreatedAt import fr.dcproject.common.entity.CreatedAt
import fr.dcproject.common.entity.DeletedAt import fr.dcproject.common.entity.DeletedAt
import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.EntityI import fr.dcproject.common.entity.EntityI
import fr.dcproject.common.entity.TargetI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.auth.database.User import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.auth.database.UserForCreate import fr.dcproject.component.auth.database.UserForCreate
@@ -96,10 +95,10 @@ open class CitizenRefWithUser(
open class CitizenRef( open class CitizenRef(
id: UUID = UUID.randomUUID() id: UUID = UUID.randomUUID()
) : TargetRef(id), ) : Entity(id),
CitizenI CitizenI
interface CitizenI : EntityI, TargetI { interface CitizenI : EntityI {
data class Name( data class Name(
override val firstName: String, override val firstName: String,
override val lastName: String, override val lastName: String,

View File

@@ -21,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: CommentI, parent: CommentForView<T, CitizenCreatorI>,
page: Int = 1, page: Int = 1,
limit: Int = 50 limit: Int = 50
): Paginated<CommentForView<T, CitizenCreatorI>> { ): Paginated<CommentForView<T, CitizenCreatorI>> {

View File

@@ -4,7 +4,6 @@ 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
@@ -22,13 +21,11 @@ import java.util.UUID
object GetCommentChildren { object GetCommentChildren {
@Location("/comments/{comment}/children") @Location("/comments/{comment}/children")
class CommentChildrenRequest( class CommentChildrenRequest(
comment: UUID, val 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

@@ -7,8 +7,10 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get
import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI
fun Route.definition() { fun Route.definition() {
get("/") { get("/") {
call.respondText("/openapi.yaml".readResource(), ContentType("text", "yaml")) call.respondText("/openapi.yaml".readResource(), ContentType("text", "yaml"))

View File

@@ -3,8 +3,10 @@ package fr.dcproject.component.doc.routes
import io.ktor.auth.authenticate import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@KtorExperimentalAPI
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Routing.installDocRoutes() { fun Routing.installDocRoutes() {

View File

@@ -1,13 +1,11 @@
package fr.dcproject.component.follow package fr.dcproject.component.follow
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository
import org.koin.dsl.module import org.koin.dsl.module
val followKoinModule = module { val followKoinModule = module {
single { FollowArticleRepository(get()) } single { FollowArticleRepository(get()) }
single { FollowConstitutionRepository(get()) } single { FollowConstitutionRepository(get()) }
single { FollowCitizenRepository(get()) }
single { FollowAccessControl() } single { FollowAccessControl() }
} }

View File

@@ -4,9 +4,7 @@ import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.TargetRef import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.constitution.database.ConstitutionForView import fr.dcproject.component.constitution.database.ConstitutionForView
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
@@ -74,24 +72,21 @@ sealed class FollowRepository<IN : TargetRef, OUT : TargetRef>(override var requ
target: Entity, target: Entity,
bulkSize: Int = 300 bulkSize: Int = 300
): Flow<FollowForView<IN>> = flow { ): Flow<FollowForView<IN>> = flow {
var lastId: UUID? = null var nextPage = 1
while (true) { do {
val result = findFollowsByTarget(target, lastId, bulkSize) val paginate = findFollowsByTarget(target, nextPage, bulkSize)
if (result.count() == 0) { paginate.result.forEach {
break
}
result.forEach {
emit(it) emit(it)
} }
lastId = result.last().id nextPage = paginate.currentPage + 1
} } while (!paginate.isLastPage())
} }
abstract fun findFollowsByTarget( abstract fun findFollowsByTarget(
target: Entity, target: Entity,
lastId: UUID?, page: Int = 1,
limit: Int = 300 limit: Int = 300
): List<FollowForView<IN>> ): Paginated<FollowForView<IN>>
} }
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) { class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
@@ -112,14 +107,14 @@ class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRe
override fun findFollowsByTarget( override fun findFollowsByTarget(
target: Entity, target: Entity,
lastId: UUID?, page: Int,
limit: Int limit: Int
): List<FollowForView<ArticleRef>> { ): Paginated<FollowForView<ArticleRef>> {
return requester return requester
.getFunction("find_follows_article_by_target") .getFunction("find_follows_article_by_target")
.select( .select(
"start_id" to lastId, page,
"limit" to limit, limit,
"target_id" to target.id "target_id" to target.id
) )
} }
@@ -143,34 +138,9 @@ class FollowConstitutionRepository(requester: Requester) : FollowRepository<Cons
override fun findFollowsByTarget( override fun findFollowsByTarget(
target: Entity, target: Entity,
lastId: UUID?,
limit: Int
): List<FollowForView<ConstitutionRef>> {
TODO("Not yet implemented")
}
}
class FollowCitizenRepository(requester: Requester) : FollowRepository<CitizenRef, Citizen>(requester) {
override fun findByCitizen(
citizenId: UUID,
page: Int, page: Int,
limit: Int limit: Int
): Paginated<FollowForView<Citizen>> { ): Paginated<FollowForView<ConstitutionRef>> {
return requester.run {
getFunction("find_follows_citizen_by_citizen")
.select(
page,
limit,
"created_by_id" to citizenId
)
}
}
override fun findFollowsByTarget(
target: Entity,
lastId: UUID?,
limit: Int
): List<FollowForView<CitizenRef>> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View File

@@ -7,7 +7,6 @@ import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.routes.citizen.toOutput
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
@@ -15,6 +14,7 @@ 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 org.joda.time.DateTime
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@@ -30,7 +30,19 @@ object GetFollowArticle {
ac.assert { canView(follow, citizenOrNull) } ac.assert { canView(follow, citizenOrNull) }
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,
follow.toOutput() follow.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
) )
} ?: call.respond(HttpStatusCode.NoContent) } ?: call.respond(HttpStatusCode.NoContent)
} }

View File

@@ -1,36 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowForUpdate
import io.ktor.application.call
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 FollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.followCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
post<CitizenFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
ac.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}
}
}

View File

@@ -1,37 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object GetFollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.getFollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
get<CitizenFollowRequest> {
repo.findFollow(citizen, it.citizen)?.let { follow ->
ac.assert { canView(follow, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
follow.toOutput()
)
} ?: call.respond(HttpStatusCode.NoContent)
}
}
}

View File

@@ -1,50 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import org.joda.time.DateTime
import java.util.UUID
@KtorExperimentalLocationsAPI
object GetMyFollowsCitizen {
@Location("/citizens/{citizen}/follows/citizens")
class CitizenFollowCitizenRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.getMyFollowsCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
get<CitizenFollowCitizenRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen)
ac.assert { canView(follows.result, citizenOrNull) }
call.respond(
HttpStatusCode.OK,
follows.toOutput { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
)
}
}
}

View File

@@ -1,36 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowForUpdate
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.delete
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.UUID
@KtorExperimentalLocationsAPI
object UnfollowCitizen {
@Location("/citizens/{citizen}/follows")
class CitizenFollowRequest(citizen: UUID) {
val citizen = CitizenRef(citizen)
}
fun Route.unfollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) {
delete<CitizenFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
ac.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}
}
}

View File

@@ -1,20 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.component.follow.routes.citizen.FollowCitizen.followCitizen
import fr.dcproject.component.follow.routes.citizen.GetFollowCitizen.getFollowCitizen
import fr.dcproject.component.follow.routes.citizen.GetMyFollowsCitizen.getMyFollowsCitizen
import fr.dcproject.component.follow.routes.citizen.UnfollowCitizen.unfollowCitizen
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installFollowCitizenRoutes() {
authenticate(optional = true) {
followCitizen(get(), get())
unfollowCitizen(get(), get())
getFollowCitizen(get(), get())
getMyFollowsCitizen(get(), get())
}
}

View File

@@ -1,20 +0,0 @@
package fr.dcproject.component.follow.routes.citizen
import fr.dcproject.common.response.toOutput
import fr.dcproject.component.follow.database.FollowForView
import org.joda.time.DateTime
import java.util.UUID
fun FollowForView<*>.toOutput(): Any = this.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}

View File

@@ -7,7 +7,6 @@ import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.routes.citizen.toOutput
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
@@ -15,6 +14,7 @@ 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 org.joda.time.DateTime
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@@ -30,7 +30,19 @@ object GetFollowConstitution {
ac.assert { canView(follow, citizenOrNull) } ac.assert { canView(follow, citizenOrNull) }
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,
follow.toOutput() follow.let { f ->
object {
val id: UUID = f.id
val createdBy: Any = f.createdBy.toOutput()
val target: Any = f.target.let { t ->
object {
val id: UUID = t.id
val reference: String = f.target.reference
}
}
val createdAt: DateTime = f.createdAt
}
}
) )
} ?: call.respond(HttpStatusCode.NotFound) } ?: call.respond(HttpStatusCode.NotFound)
} }

View File

@@ -11,14 +11,16 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import fr.dcproject.common.entity.Entity import fr.dcproject.common.entity.Entity
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import io.konform.validation.Validation
import io.konform.validation.jsonschema.enum
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) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
@JsonSubTypes( @JsonSubTypes(
JsonSubTypes.Type(value = ArticleUpdateNotificationMessage::class, name = "article") JsonSubTypes.Type(value = ArticleUpdateNotification::class, name = "article")
) )
open class NotificationMessage( open class Notification(
val type: String, val type: String,
val createdAt: DateTime = DateTime.now() val createdAt: DateTime = DateTime.now()
) { ) {
@@ -48,16 +50,24 @@ open class NotificationMessage(
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
} }
inline fun <reified T : NotificationMessage> fromString(raw: String): T = mapper.readValue(raw) inline fun <reified T : Notification> fromString(raw: String): T = mapper.readValue(raw)
}
fun getValidation() = Validation<Notification> {
Notification::type {
enum(
"article"
)
}
} }
} }
open class EntityNotificationMessage <E : Entity> ( open class EntityNotification(
val target: E, val target: Entity,
type: String, type: String,
val action: String val action: String
) : NotificationMessage(type) ) : Notification(type)
class ArticleUpdateNotificationMessage( class ArticleUpdateNotification(
target: ArticleForView target: ArticleForView
) : EntityNotificationMessage<ArticleForView>(target, "article", "update") ) : EntityNotification(target, "article", "update")

View File

@@ -0,0 +1,112 @@
package fr.dcproject.component.notification
import com.rabbitmq.client.AMQP.BasicProperties
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import com.rabbitmq.client.ConnectionFactory
import com.rabbitmq.client.Consumer
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.Envelope
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.database.FollowForView
import io.ktor.utils.io.errors.IOException
import io.lettuce.core.RedisClient
import io.lettuce.core.api.async.RedisAsyncCommands
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationConsumer(
private val rabbitFactory: ConnectionFactory,
private val redisClient: RedisClient,
private val followConstitutionRepo: FollowConstitutionRepository,
private val followArticleRepo: FollowArticleRepository,
private val notificationEmailSender: NotificationEmailSender,
private val exchangeName: String,
) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
private val rabbitConnection = rabbitFactory.newConnection()
private val rabbitChannel = rabbitConnection.createChannel()
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
fun close() {
rabbitChannel.close()
rabbitConnection.close()
}
fun start() {
/* Config Rabbit */
rabbitFactory.newConnection().use { connection ->
connection.createChannel().use { channel ->
channel.queueDeclare("push", true, false, false, null)
channel.queueDeclare("email", true, false, false, null)
channel.exchangeDeclare(exchangeName, DIRECT, true)
channel.queueBind("push", exchangeName, "")
channel.queueBind("email", exchangeName, "")
}
}
/* Define Consumer */
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: BasicProperties,
body: ByteArray
) = runBlocking {
followersFromMessage(body) {
redis.zadd(
"notification:${it.follow.createdBy.id}",
it.event.id,
it.rawMessage
)
}
rabbitChannel.basicAck(envelope.deliveryTag, false)
}
}
val consumerEmail: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class)
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: BasicProperties,
body: ByteArray
) {
runBlocking {
followersFromMessage(body) {
notificationEmailSender.sendEmail(it.follow)
logger.debug("EmailSend to: ${it.follow.createdBy.id}")
}
}
rabbitChannel.basicAck(envelope.deliveryTag, false)
}
}
/* Launch Consumer */
rabbitChannel.basicConsume("push", false, consumerPush) // The front consume the redis via Websocket
rabbitChannel.basicConsume("email", false, consumerEmail)
}
private suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage) -> Unit) {
val rawMessage: String = body.toString(Charsets.UTF_8)
val notification: EntityNotification = Notification.fromString(rawMessage)
val follows = when (notification.type) {
"article" -> followArticleRepo.findFollowsByTarget(notification.target)
"constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target)
else -> error("event '${notification.type}' not implemented")
}
follows.collect { action(DecodedMessage(notification, rawMessage, it)) }
}
private class DecodedMessage(
val event: EntityNotification,
val rawMessage: String,
val follow: FollowForView<out TargetRef>
)
}

View File

@@ -1,67 +0,0 @@
package fr.dcproject.component.notification
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import com.rabbitmq.client.ConnectionFactory
import com.rabbitmq.client.DefaultConsumer
import fr.dcproject.common.entity.Entity
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.common.utils.consumeQueue
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.database.FollowForView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
abstract class NotificationConsumerAbstract(
private val rabbitFactory: ConnectionFactory,
private val followConstitutionRepo: FollowConstitutionRepository,
private val followArticleRepo: FollowArticleRepository,
private val followCitizenRepo: FollowCitizenRepository,
) {
private val rabbitConnection = rabbitFactory.newConnection()
private val rabbitChannel = rabbitConnection.createChannel()
fun close() {
rabbitChannel.close()
rabbitConnection.close()
}
fun declareQueue(queueName: String, exchangeName: String) {
rabbitFactory.newConnection().use { connection ->
connection.createChannel().use { channel ->
channel.queueDeclare(queueName, true, false, false, null)
channel.exchangeDeclare(exchangeName, DIRECT, true)
channel.queueBind(queueName, exchangeName, "")
}
}
}
protected fun consumeQueue(queueName: String, callback: DefaultConsumer.(DecodedMessage<*>) -> Unit) =
rabbitChannel.consumeQueue(queueName) { body ->
runBlocking {
followersFromMessage(body) {
callback(it)
}
}
}
protected suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage<*>) -> Unit) {
val rawMessage: String = body.toString(Charsets.UTF_8)
val notification: EntityNotificationMessage<*> = NotificationMessage.fromString(rawMessage)
val follows = when (notification.type) {
"article" -> followArticleRepo.findFollowsByTarget(notification.target)
"constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target)
"citizen" -> followCitizenRepo.findFollowsByTarget(notification.target)
else -> error("event '${notification.type}' not implemented")
}
follows.collect { action(DecodedMessage(notification, rawMessage, it)) }
}
protected class DecodedMessage <E : Entity> (
val event: EntityNotificationMessage<E>,
val rawMessage: String,
val follow: FollowForView<out TargetRef>
)
}

View File

@@ -0,0 +1,70 @@
package fr.dcproject.component.notification
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.common.email.Mailer
import fr.dcproject.common.entity.EntityI
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.article.database.ArticleWithTitleI
import fr.dcproject.component.citizen.database.CitizenCreatorI
import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.component.follow.database.FollowForView
import java.util.UUID
class NotificationEmailSender(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository,
private val articleRepo: ArticleRepository
) {
fun sendEmail(follow: FollowForView<out TargetRef>) {
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
val target = when (follow.target.reference) {
"article" ->
articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
else -> noTarget(follow.target.id)
}
val subject = when (follow.target.reference) {
"article" -> """New version for article "${target.title}""""
else -> "Notification"
}
mailer.sendEmail {
Mail(
Email("notification@$domain"),
subject,
Email(citizen.email),
Content("text/plain", generateContent(citizen, target))
).apply {
addContent(Content("text/html", generateHtmlContent(citizen, target)))
}
}
}
private fun generateHtmlContent(citizen: CitizenCreatorI, target: EntityI): String? {
return when (target) {
is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent()
else -> noTarget(target.id)
}
}
private fun generateContent(citizen: CitizenCreatorI, target: EntityI): String {
return when (target) {
is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent()
else -> noTarget(target.id)
}
}
class NoCitizen(message: String) : Exception(message)
class NoTarget(message: String) : Exception(message)
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
}

View File

@@ -1,9 +1,9 @@
package fr.dcproject.component.notification.push package fr.dcproject.component.notification
import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.core.JsonProcessingException
import fr.dcproject.application.http.badRequestIfNotValid
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.notification.NotificationMessage
import io.ktor.http.cio.websocket.Frame import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.Frame.Text import io.ktor.http.cio.websocket.Frame.Text
import io.ktor.http.cio.websocket.readText import io.ktor.http.cio.websocket.readText
@@ -29,42 +29,34 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/** class NotificationsPush (
* Listen a custom flow to mark as read a message.
*
* And listen the redis subscription flow and call a callback when a new message arrives
*/
class NotificationPushListener(
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<NotificationMessage>, incoming: Flow<Notification>,
onReceive: suspend (NotificationMessage) -> Unit, onReceive: suspend (Notification) -> Unit,
) { ) {
class Builder(redisClient: RedisClient) { class Builder(val redisClient: RedisClient) {
private val redisConnection = redisClient.connect() private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redisConnectionPubSub = redisClient.connectPubSub() private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis PubSub")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis Async")
/**
* Build Listener with citizen, incoming flow and set an outgoing callback
*/
fun build( fun build(
citizen: CitizenI, citizen: CitizenI,
incoming: Flow<NotificationMessage>, incoming: Flow<Notification>,
onReceive: suspend (NotificationMessage) -> Unit, onReceive: suspend (Notification) -> Unit,
): NotificationPushListener = NotificationPushListener(redis, redisConnectionPubSub, citizen, incoming, onReceive) ): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onReceive)
/**
* Build NotificationPush with only a WebSocket session
*/
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
fun build(ws: DefaultWebSocketServerSession): NotificationPushListener { fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
/* Convert channel of string from websocket, to a flow of Notification object */ /* Convert channel of string from websocket, to a flow of Notification object */
val incomingFlow: Flow<NotificationMessage> = ws.incoming.consumeAsFlow() val incomingFlow: Flow<Notification> = ws.incoming.consumeAsFlow()
.mapNotNull<Frame, Text> { it as? Text } .mapNotNull<Frame, Text> { it as? Frame.Text }
.map { it.readText() } .map { it.readText() }
.map { NotificationMessage.fromString(it) } .map {
Notification.fromString<Notification>(it)
.apply { getValidation().validate(this).badRequestIfNotValid() }
}
return build(ws.call.citizen, incomingFlow) { return build(ws.call.citizen, incomingFlow) {
ws.outgoing.send(Text(it.toString())) ws.outgoing.send(Text(it.toString()))
@@ -74,42 +66,30 @@ class NotificationPushListener(
} }
} }
/**
* The key of the SortedSet in Redis which contains all the messages of a user
*/
private val key = "notification:${citizen.id}" private val key = "notification:${citizen.id}"
/** private var score: Double = 0.0
* The last score (a kind of sorted ids) of message
*/
private var lastScore: Double = 0.0
/**
* Configure the listener to listen all new notifications
*/
private val listener = object : RedisPubSubAdapter<String, String>() { private val listener = object : RedisPubSubAdapter<String, String>() {
/* On new key publish */ /* On new key publish */
override fun message(pattern: String?, channel: String?, message: String?) { override fun message(pattern: String?, channel: String?, message: String?) {
runBlocking { runBlocking {
getNewUnreadNotifications().collect { getNotifications().collect {
onReceive(it) onReceive(it)
} }
} }
} }
} }
/**
* Init the listener and the callback
*/
init { init {
/* Mark as read all incoming notifications */ /* Mark as read all incoming notifications */
GlobalScope.launch { GlobalScope.launch {
incoming.collect { incoming.collect {
it.markAsRead() markAsRead(it)
} }
} }
/* Get old notification and sent it to websocket */ /* Get old notification and sent it to websocket */
runBlocking { runBlocking {
getNewUnreadNotifications().collect { getNotifications().collect {
onReceive(it) onReceive(it)
} }
} }
@@ -119,55 +99,38 @@ class NotificationPushListener(
addListener(listener) addListener(listener)
/* Register to the events */ /* Register to the events */
async()?.psubscribe("__key*__:$key") async()?.psubscribe("__key*__:$key") ?: error("Unable to subscribe to redis events")
} }
} }
/**
* Close the redis subscription
*/
fun close() { fun close() {
redisConnectionPubSub.removeListener(listener) redisConnectionPubSub.removeListener(listener)
} }
/** /* Return flow with all new notifications */
* Get All new notification from redis and private fun getNotifications() = flow<Notification> {
* Return flow with notifications
*
* On start, on the first call, this method return all unread notification of the user
*
* Internally this method return all messages that greater of the lastScore,
* then define the lastScore with the score of the last message.
*/
private fun getNewUnreadNotifications() = flow<NotificationMessage> {
redis redis
.zrangebyscoreWithScores( .zrangebyscoreWithScores(
key, key,
Range.from( Range.from(
Boundary.excluding(lastScore), Boundary.excluding(score),
Boundary.including(Double.POSITIVE_INFINITY) Boundary.including(Double.POSITIVE_INFINITY)
), ),
Limit.from(100) Limit.from(100)
) )
.get().forEach { .get().forEach {
/* Build message object from raw string and return it */ emit(Notification.fromString(it.value))
emit(NotificationMessage.fromString(it.value)) if (it.score > score) score = it.score
if (it.score > lastScore) lastScore = it.score
} }
} }
/** private suspend fun markAsRead(notificationMessage: Notification) = coroutineScope {
* Mark one notification as read.
*
* Internally, this method remove the message of the SortedSet in redis
*/
private suspend fun NotificationMessage.markAsRead() = coroutineScope {
try { try {
redis.zremrangebyscore( redis.zremrangebyscore(
key, key,
Range.from( Range.from(
Boundary.including(id), Boundary.including(notificationMessage.id),
Boundary.including(id) Boundary.including(notificationMessage.id)
) )
) )
} catch (e: JsonProcessingException) { } catch (e: JsonProcessingException) {

View File

@@ -7,15 +7,12 @@ import kotlinx.coroutines.coroutineScope
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
class NotificationPublisherAsync( class Publisher(
private val factory: ConnectionFactory, private val factory: ConnectionFactory,
private val logger: Logger = LoggerFactory.getLogger(NotificationPublisherAsync::class.qualifiedName), private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName),
private val exchangeName: String, private val exchangeName: String,
) { ) {
/** suspend fun <T : EntityNotification> publish(it: T): Deferred<Unit> = coroutineScope {
* Publish a new notification message to RabbitMQ
*/
suspend fun <T : EntityNotificationMessage<*>> publishAsync(it: T): Deferred<Unit> = coroutineScope {
async { async {
factory.newConnection().use { connection -> factory.newConnection().use { connection ->
connection.createChannel().use { channel -> connection.createChannel().use { channel ->

View File

@@ -1,33 +0,0 @@
package fr.dcproject.component.notification.email
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.notification.NotificationConsumerAbstract
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationEmailConsumer(
rabbitFactory: ConnectionFactory,
followConstitutionRepo: FollowConstitutionRepository,
followArticleRepo: FollowArticleRepository,
followCitizenRepo: FollowCitizenRepository,
private val notificationEmailSender: NotificationEmailSender,
private val exchangeName: String,
) : NotificationConsumerAbstract(rabbitFactory, followConstitutionRepo, followArticleRepo, followCitizenRepo) {
private val logger: Logger = LoggerFactory.getLogger(NotificationEmailConsumer::class.qualifiedName)
fun start() {
/* Config Rabbit */
declareQueue(QUEUE_NAME, exchangeName)
consumeQueue(QUEUE_NAME) { message ->
notificationEmailSender.sendEmail(message.follow)
logger.debug("EmailSend to: ${message.follow.createdBy.id}")
}
}
companion object {
private const val QUEUE_NAME = "email"
}
}

View File

@@ -1,70 +0,0 @@
package fr.dcproject.component.notification.email
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.common.email.Mailer
import fr.dcproject.common.entity.TargetRef
import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenRepository
import fr.dcproject.component.follow.database.FollowForView
import fr.dcproject.component.notification.email.content.ArticleNotificationEmailContent
import fr.dcproject.component.notification.email.content.CitizenNotificationEmailContent
import java.util.UUID
/**
* Send notification email on the follower
*/
class NotificationEmailSender(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository,
private val articleRepo: ArticleRepository
) {
/**
* Send the Notification Email to the follower user
*/
fun sendEmail(follow: FollowForView<out TargetRef>) {
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
/**
* Find the complete target entity by its ID according to its reference
*/
val target = when (follow.target.reference) {
"article" -> articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
"citizen" -> citizenRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
else -> noTarget(follow.target.id)
}
/**
* Find content of the email according to the target type
*/
val content = when (target) {
is ArticleForView -> ArticleNotificationEmailContent(citizen, target, domain)
is Citizen -> CitizenNotificationEmailContent(citizen, target, domain)
else -> noTargetTypeImplementation(follow.target.reference)
}
/* Send email */
mailer.sendEmail {
Mail(
Email("notification@$domain"),
content.subject,
Email(citizen.email),
Content("text/plain", content.content)
).apply {
addContent(Content("text/html", content.contentHtml))
}
}
}
class NoCitizen(message: String) : Exception(message)
class NoTarget(message: String) : Exception(message)
class NoTargetTypeImplement(message: String) : Exception(message)
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
private fun noTargetTypeImplementation(type: String): Nothing = throw NoTargetTypeImplement("No Target type implemented: $type")
}

View File

@@ -1,29 +0,0 @@
package fr.dcproject.component.notification.email.content
import fr.dcproject.component.article.database.ArticleWithTitleI
import fr.dcproject.component.citizen.database.Citizen
class ArticleNotificationEmailContent(
private val citizen: Citizen,
private val target: ArticleWithTitleI,
private val domain: String,
) : NotificationEmailContent {
override val subject: String
get() = """New version for article "${target.title}""""
override val contentHtml
get() = run {
"""
Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent()
}
override val content
get() = run {
"""
Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent()
}
}

View File

@@ -1,28 +0,0 @@
package fr.dcproject.component.notification.email.content
import fr.dcproject.component.citizen.database.Citizen
class CitizenNotificationEmailContent(
private val citizen: Citizen,
private val target: Citizen,
private val domain: String,
) : NotificationEmailContent {
override val subject: String
get() = """New activity for the citizen "${target.name}""""
override val contentHtml
get() = run {
"""
Hello ${citizen.name.getFullName()},
The citizen "${target.name}" was new activity, check it here: <a href="http://$domain/citizens/${target.id}">here</a>
""".trimIndent()
}
override val content
get() = run {
"""
Hello ${citizen.name.getFullName()},
The citizen "${target.name}" was new activity, check it here: http://$domain/citizens/${target.id}
""".trimIndent()
}
}

View File

@@ -1,7 +0,0 @@
package fr.dcproject.component.notification.email.content
interface NotificationEmailContent {
val subject: String
val content: String
val contentHtml: String
}

View File

@@ -1,41 +0,0 @@
package fr.dcproject.component.notification.push
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.notification.NotificationConsumerAbstract
import io.lettuce.core.RedisClient
import io.lettuce.core.api.async.RedisAsyncCommands
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class NotificationPushConsumer(
rabbitFactory: ConnectionFactory,
followConstitutionRepo: FollowConstitutionRepository,
followArticleRepo: FollowArticleRepository,
followCitizenRepo: FollowCitizenRepository,
redisClient: RedisClient,
private val exchangeName: String,
) : NotificationConsumerAbstract(rabbitFactory, followConstitutionRepo, followArticleRepo, followCitizenRepo) {
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
private val logger: Logger = LoggerFactory.getLogger(NotificationPushConsumer::class.qualifiedName)
fun start() {
/* Config Rabbit */
declareQueue(QUEUE_NAME, exchangeName)
consumeQueue(QUEUE_NAME) { message ->
redis.zadd(
"notification:${message.follow.createdBy.id}",
message.event.id,
message.rawMessage
)
logger.debug("Notification was transferred to the redis (follower: ${message.follow.createdBy.id})")
}
}
companion object {
private const val QUEUE_NAME = "push"
}
}

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.notification.routes package fr.dcproject.component.notification.routes
import fr.dcproject.component.notification.push.NotificationPushListener import fr.dcproject.component.notification.NotificationsPush
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.websocket.webSocket import io.ktor.websocket.webSocket
@@ -13,8 +13,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.notificationArticle(pushListenerBuilder: NotificationPushListener.Builder) { fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) {
webSocket("/notifications") { webSocket("/notifications") {
pushListenerBuilder.build(this) pushBuilder.build(this)
} }
} }

View File

@@ -1,6 +1,5 @@
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
@@ -13,9 +12,6 @@ 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
@@ -38,22 +34,11 @@ 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,6 +1,5 @@
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
@@ -10,9 +9,6 @@ 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
@@ -32,22 +28,11 @@ 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,6 +1,5 @@
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
@@ -11,9 +10,6 @@ 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
@@ -29,22 +25,13 @@ 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,21 +1,15 @@
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
@@ -27,29 +21,18 @@ import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object PutVoteOnComment { object PutVoteOnComment {
@Location("/comments/{comment}/vote") @Location("/comments/{comment}/vote")
class CommentVoteRequest(comment: UUID) { class CommentVoteRequest(val comment: UUID) {
val comment = CommentRef(comment) data class Content(var note: Int)
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 comment = commentRepo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found") val content = call.receiveOrBadRequest<CommentVoteRequest.Content>()
val input = call.receiveOrBadRequest<CommentVoteRequest.Input>()
.apply { validate().badRequestIfNotValid() }
val vote = VoteForUpdate( val vote = VoteForUpdate(
target = comment, target = comment,
note = input.note, note = content.note,
createdBy = this.citizen createdBy = this.citizen
) )
ac.assert { canCreate(vote, citizenOrNull) } ac.assert { canCreate(vote, citizenOrNull) }

View File

@@ -1,6 +1,5 @@
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
@@ -12,9 +11,6 @@ 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
@@ -30,25 +26,17 @@ 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 input = call.receiveOrBadRequest<Input>() val content = call.receiveOrBadRequest<Input>()
.apply { validate().badRequestIfNotValid() }
val vote = VoteForUpdate( val vote = VoteForUpdate(
target = constitution, target = constitution,
note = input.note, note = content.note,
createdBy = this.citizen createdBy = this.citizen
) )
ac.assert { canCreate(vote, citizenOrNull) } ac.assert { canCreate(vote, citizenOrNull) }

View File

@@ -1,9 +1,8 @@
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.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
@@ -11,9 +10,6 @@ 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
@@ -33,30 +29,13 @@ 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,18 +1,13 @@
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
@@ -20,33 +15,19 @@ import io.ktor.locations.Location
import io.ktor.locations.put import io.ktor.locations.put
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object EditWorkgroup { object EditWorkgroup {
@Location("/workgroups/{workgroupId}") @Location("/workgroups/{workgroupId}")
class PutWorkgroupRequest(val workgroupId: UUID) { class PutWorkgroupRequest(val workgroupId: UUID) : KoinComponent {
class Input( class Input(
val name: String?, val name: String?,
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) {
@@ -54,7 +35,6 @@ 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,20 +1,12 @@
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
@@ -35,33 +27,16 @@ 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> {
it.validate().badRequestIfNotValid() val workgroups =
repo.find(
val workgroups = repo.find(
it.page, it.page,
it.limit, it.limit,
it.sort, it.sort,

View File

@@ -17,12 +17,13 @@ import io.ktor.locations.Location
import io.ktor.locations.post import io.ktor.locations.post
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object AddMemberToWorkgroup { object AddMemberToWorkgroup {
@Location("/workgroups/{workgroupId}/members") @Location("/workgroups/{workgroupId}/members")
class WorkgroupsMembersRequest(val workgroupId: UUID) { class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
class Input : MutableList<Input.Member> by mutableListOf() { class Input : MutableList<Input.Member> by mutableListOf() {
class Member(val citizen: CitizenRef, roles: List<String> = emptyList()) { class Member(val citizen: CitizenRef, roles: List<String> = emptyList()) {
val roles: List<WorkgroupWithMembersI.Member.Role> = roles.map { val roles: List<WorkgroupWithMembersI.Member.Role> = roles.map {

View File

@@ -17,13 +17,14 @@ import io.ktor.locations.Location
import io.ktor.locations.delete import io.ktor.locations.delete
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID import java.util.UUID
import fr.dcproject.component.workgroup.routes.members.DeleteMembersOfWorkgroup.WorkgroupsMembersRequest.Input as Input import fr.dcproject.component.workgroup.routes.members.DeleteMembersOfWorkgroup.WorkgroupsMembersRequest.Input as Input
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object DeleteMembersOfWorkgroup { object DeleteMembersOfWorkgroup {
@Location("/workgroups/{workgroupId}/members") @Location("/workgroups/{workgroupId}/members")
class WorkgroupsMembersRequest(val workgroupId: UUID) { class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
class Input : MutableList<Input.Member> by mutableListOf() { class Input : MutableList<Input.Member> by mutableListOf() {
class Member(val citizen: CitizenRef) class Member(val citizen: CitizenRef)
} }

View File

@@ -17,12 +17,13 @@ import io.ktor.locations.Location
import io.ktor.locations.put import io.ktor.locations.put
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object UpdateMemberOfWorkgroup { object UpdateMemberOfWorkgroup {
@Location("/workgroups/{workgroupId}/members") @Location("/workgroups/{workgroupId}/members")
class WorkgroupsMembersRequest(val workgroupId: UUID) { class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
class Input : MutableList<Input.Item> by mutableListOf() { class Input : MutableList<Input.Item> by mutableListOf() {
class Item(val citizen: CitizenRef, roles: List<String> = emptyList()) { class Item(val citizen: CitizenRef, roles: List<String> = emptyList()) {
val roles: List<WorkgroupWithMembersI.Member.Role> = roles.map { val roles: List<WorkgroupWithMembersI.Member.Role> = roles.map {

View File

@@ -170,18 +170,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/404' $ref: '#/components/schemas/404'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/401'
403:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/403'
/articles/{article}/versions: /articles/{article}/versions:
parameters: parameters:
@@ -955,105 +943,13 @@ paths:
description: Return only http status 204 on success description: Return only http status 204 on success
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
/citizens/{citizen}/follows:
parameters:
- $ref: '#/components/parameters/citizen'
get:
security:
- JWTAuth: [ ]
summary: Return Follows of citizen
tags:
- follow
- citizen
responses:
200:
description: Return follows
content:
application/json:
schema:
$ref: '#/components/schemas/FollowResponse'
404:
description: Citizen not exist
content:
application/json:
schema:
$ref: '#/components/schemas/404'
post:
security:
- JWTAuth: []
summary: Follow citizen
description: Follow a citizen to receive notifications of his activity
tags:
- follow
- citizen
responses:
201:
description: Return only http status 201 on success
401:
$ref: '#/components/responses/401'
404:
description: Citizen not exist
content:
application/json:
schema:
$ref: '#/components/schemas/404'
delete:
security:
- JWTAuth: [ ]
summary: Unfollow one citizen
tags:
- follow
- citizen
responses:
204:
description: Return only http status 204 on success
401:
$ref: '#/components/responses/401'
404:
description: Citizen not exist
content:
application/json:
schema:
$ref: '#/components/schemas/404'
/citizens/{citizen}/follows/citizens:
parameters:
- $ref: '#/components/parameters/citizen'
get:
security:
- JWTAuth: [ ]
summary: Return citizen Follow of citizen
tags:
- follow
- citizen
responses:
200:
description: Return citizen Follow of citizen
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Paginated'
- type: object
properties:
result:
type: array
items:
$ref: '#/components/schemas/FollowResponse'
404:
description: Citizen not exist
content:
application/json:
schema:
$ref: '#/components/schemas/404'
/citizens/{citizen}/follows/articles: /citizens/{citizen}/follows/articles:
parameters: parameters:
- $ref: '#/components/parameters/citizen' - $ref: '#/components/parameters/citizen'
get: get:
security: security:
- JWTAuth: [ ] - JWTAuth: [ ]
summary: Return article Follow of citizen summary: Return Follow or nothing if you not follow
tags: tags:
- follow - follow
- article - article
@@ -1128,7 +1024,7 @@ paths:
- citizen - citizen
responses: responses:
200: 200:
description: Return constitution Follow of citizen description: Return your follows
content: content:
application/json: application/json:
schema: schema:
@@ -1222,9 +1118,6 @@ paths:
tags: tags:
- opinion - opinion
- citizen - citizen
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
responses: responses:
200: 200:
description: Opinions description: Opinions
@@ -1239,13 +1132,6 @@ 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'
@@ -1298,12 +1184,6 @@ 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'
@@ -1362,12 +1242,6 @@ 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:
@@ -1381,9 +1255,6 @@ paths:
- vote - vote
- article - article
- citizen - citizen
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
responses: responses:
200: 200:
description: Votes description: Votes
@@ -1398,12 +1269,6 @@ 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:
@@ -1428,12 +1293,6 @@ 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'
@@ -1463,12 +1322,6 @@ 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:
@@ -1507,12 +1360,6 @@ 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'
@@ -1561,12 +1408,6 @@ 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:
@@ -2572,20 +2413,7 @@ components:
reason: reason:
type: string type: string
example: 'Cannot be null' example: 'Cannot be null'
403:
description: Forbiden
properties:
statusCode:
type: integer
title:
type: string
401:
description: Unauthorized
properties:
statusCode:
type: integer
title:
type: string
404: 404:
description: Not Found description: Not Found
required: required:

View File

@@ -23,7 +23,6 @@ begin
select select
a.id, a.id,
a.title, a.title,
a.created_at,
a.deleted_at, a.deleted_at,
a.draft, a.draft,
find_citizen_by_id_with_user(a.created_by_id) as created_by, find_citizen_by_id_with_user(a.created_by_id) as created_by,

View File

@@ -1,21 +1,20 @@
create or replace function find_follows_article_by_target( create or replace function find_follows_article_by_target(
_target_id uuid, _target_id uuid,
_limit int default 50, "limit" int default 50,
_start_id uuid default null, "offset" int default 0,
out resource json out resource json,
out total int
) language plpgsql as ) language plpgsql as
$$ $$
declare declare
_version_id uuid = (select version_id from article where id = _target_id); _version_id uuid = (select version_id from article where id = _target_id);
_start_at timestamp default '2000-01-01 00:00:00'::timestamp;
_article_creator_id uuid = (select created_by_id from article where id = _target_id);
begin begin
if _start_id is not null then select json_agg(t), (
select created_at into _start_at from follow where id = _start_id; select count(f.id)
end if; from follow f
join article a on f.target_id = a.id
select json_agg(t) where a.version_id = _version_id)
into resource into resource, total
from ( from (
select select
f.id, f.id,
@@ -23,17 +22,11 @@ begin
f.target_reference, f.target_reference,
json_build_object('id', f.target_id) as target, json_build_object('id', f.target_id) as target,
find_citizen_by_id_with_user(f.created_by_id) as created_by find_citizen_by_id_with_user(f.created_by_id) as created_by
from follow as f from follow_article as f
left join article a on f.target_reference = 'article'::regclass and f.target_id = a.id join article a on f.target_id = a.id
where ( where a.version_id = _version_id
(f.target_reference = 'article'::regclass and a.version_id = _version_id)
or
(f.target_reference = 'citizen'::regclass and f.target_id = _article_creator_id)
)
and f.created_at >= _start_at
and (_start_id is null or f.id != _start_id)
order by f.created_at order by f.created_at
limit _limit limit "limit" offset "offset"
) as t; ) as t;
end end
$$; $$;

View File

@@ -1,24 +0,0 @@
create or replace function find_follows_citizen_by_citizen(
_created_by_id uuid,
"limit" int default 50,
"offset" int default 0,
out resource json,
out total int
) language plpgsql as
$$
begin
select json_agg(t), (select count(id) from follow)
into resource, total
from (
select
f.*,
find_citizen_by_id_with_user(f.target_id) as target,
find_citizen_by_id_with_user(f.created_by_id) as created_by
from follow as f
where created_by_id = _created_by_id
order by created_at desc,
f.created_at desc
limit "limit" offset "offset"
) as t;
end;
$$;

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 'createdAt', sort text default 'created_at',
"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 'createdAt' then w.created_at::text when 'created_at' 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 'createdAt' then w.created_at::text when 'created_at' then w.created_at::text
end end
end end
desc, desc,

View File

@@ -8,19 +8,22 @@ import fr.dcproject.application.module
import fr.dcproject.common.email.Mailer import fr.dcproject.common.email.Mailer
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.server.testing.withTestApplication import io.ktor.server.testing.withTestApplication
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
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.TestInstance import org.junit.jupiter.api.TestInstance
import org.koin.test.AutoCloseKoinTest
import org.koin.test.KoinTest import org.koin.test.KoinTest
import org.koin.test.get import org.koin.test.get
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("functional"), Tag("mail")) @Tags(Tag("functional"), Tag("mail"))
class MailerTest : KoinTest { class MailerTest : KoinTest, AutoCloseKoinTest() {
@InternalCoroutinesApi @InternalCoroutinesApi
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Test @Test

View File

@@ -10,12 +10,12 @@ import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowForView import fr.dcproject.component.follow.database.FollowForView
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.NotificationPublisherAsync import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.email.NotificationEmailConsumer import fr.dcproject.component.notification.NotificationEmailSender
import fr.dcproject.component.notification.email.NotificationEmailSender import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.notification.push.NotificationPushConsumer
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@@ -62,9 +62,10 @@ class NotificationConsumerTest {
@InternalCoroutinesApi @InternalCoroutinesApi
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Test @Test
fun `can be receive article update notification when follow article`() = runBlocking { fun `can be send notification`() = runBlocking {
val config: Configuration = Configuration("application-test.conf") val config: Configuration = Configuration("application-test.conf")
/* Create mocks and spy's */ /* Create mocks and spy's */
val emailSender = mockk<NotificationEmailSender>() { val emailSender = mockk<NotificationEmailSender>() {
@@ -87,30 +88,21 @@ class NotificationConsumerTest {
} }
/* Config consumer */ /* Config consumer */
val emailConsumer = NotificationEmailConsumer( val consumer = NotificationConsumer(
rabbitFactory = rabbitFactory, rabbitFactory = rabbitFactory,
redisClient = redisClient,
followArticleRepo = followArticleRepo, followArticleRepo = followArticleRepo,
followConstitutionRepo = mockk(), // TODO test followConstitution followConstitutionRepo = mockk(),
followCitizenRepo = mockk(), // TODO test followCitizen
notificationEmailSender = emailSender, notificationEmailSender = emailSender,
exchangeName = "notification", exchangeName = "notification",
).apply { start() } ).apply { start() }
val pushConsumer = NotificationPushConsumer(
rabbitFactory = rabbitFactory,
followArticleRepo = followArticleRepo,
followConstitutionRepo = mockk(), // TODO test followConstitution
followCitizenRepo = mockk(), // TODO test followCitizen
redisClient = redisClient,
exchangeName = "notification",
).apply { start() }
/* Push message */ /* Push message */
NotificationPublisherAsync( Publisher(
factory = rabbitFactory, factory = rabbitFactory,
exchangeName = "notification", exchangeName = "notification",
).publishAsync( ).publish(
ArticleUpdateNotificationMessage( ArticleUpdateNotification(
ArticleForView( ArticleForView(
title = "MyTitle", title = "MyTitle",
content = "myContent", content = "myContent",
@@ -129,7 +121,6 @@ class NotificationConsumerTest {
verify(timeout = 2000) { emailSender.sendEmail(any()) } verify(timeout = 2000) { emailSender.sendEmail(any()) }
verify(timeout = 2000) { asyncCommand.zadd(any<String>(), any<Double>(), any<String>()) } verify(timeout = 2000) { asyncCommand.zadd(any<String>(), any<Double>(), any<String>()) }
emailConsumer.close() consumer.close()
pushConsumer.close()
} }
} }

View File

@@ -6,9 +6,9 @@ import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.NotificationMessage import fr.dcproject.component.notification.Notification
import fr.dcproject.component.notification.push.NotificationPushListener import fr.dcproject.component.notification.NotificationsPush
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.mockk.every import io.mockk.every
import io.mockk.spyk import io.mockk.spyk
@@ -68,14 +68,14 @@ internal class NotificationsPushTest {
title = "Super Title", title = "Super Title",
) )
/* Init two notification, one called before subscription, and the other after */ /* Init two notification, one called before subscription, and the other after */
val notifBeforeSubscribe = ArticleUpdateNotificationMessage(article) val notifBeforeSubscribe = ArticleUpdateNotification(article)
runBlocking { runBlocking {
delay(100) delay(100)
} }
val notifAfterSubscribe = ArticleUpdateNotificationMessage(article) val notifAfterSubscribe = ArticleUpdateNotification(article)
/* init event for emulate incoming message from websocket */ /* init event for emulate incoming message from websocket */
val event = MutableSharedFlow<NotificationMessage>() val event = MutableSharedFlow<Notification>()
val incomingFlow = event.asSharedFlow() val incomingFlow = event.asSharedFlow()
spyk(object { var counter = 0 }).run { /* Counter for count the callback of notification */ spyk(object { var counter = 0 }).run { /* Counter for count the callback of notification */
@@ -90,7 +90,7 @@ internal class NotificationsPushTest {
} }
/* Init NotificationPush system, and set assertion in callback */ /* Init NotificationPush system, and set assertion in callback */
val notificationPush = NotificationPushListener.Builder(redisClient).build(citizen, incomingFlow) { val notificationPush = NotificationsPush.Builder(redisClient).build(citizen, incomingFlow) {
counter++ counter++
if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id
else it.id `should be equal to` notifAfterSubscribe.id else it.id `should be equal to` notifAfterSubscribe.id

View File

@@ -2,7 +2,6 @@ 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
@@ -11,6 +10,7 @@ import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.server.testing.withTestApplication import io.ktor.server.testing.withTestApplication
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -20,15 +20,13 @@ 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
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@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(
@@ -77,8 +75,9 @@ class ViewTest {
article article
) )
/* Retry because ES is not sync ! */ /* Sleep because ES is not sync ! */
retry(10, 0.3.seconds) { Thread.sleep(1000)
/* Get view */ /* Get view */
val afterView = viewRepository.getViewsCount(article) val afterView = viewRepository.getViewsCount(article)
@@ -88,4 +87,3 @@ class ViewTest {
} }
} }
} }
}

View File

@@ -1,32 +1,29 @@
package integration package integration
import fr.dcproject.common.utils.toUUID import fr.dcproject.common.utils.toUUID
import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have article created by workgroup`
import integration.steps.given.`Given I have articles`
import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have draft article`
import integration.steps.given.`Given I have workgroup`
import integration.steps.given.`authenticated as`
import integration.steps.then.`And have property`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain pattern`
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.`Then the response should be`
import integration.steps.then.and
import integration.steps.then.`which contains`
import integration.steps.`when`.Validate 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`
import integration.steps.given.`Given I have article created by workgroup`
import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have articles`
import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have workgroup`
import integration.steps.given.`authenticated as`
import integration.steps.then.`And have property`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain pattern`
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 contain`
import integration.steps.then.`Then the response should be`
import integration.steps.then.`which contains`
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.Forbidden import io.ktor.http.HttpStatusCode.Companion.Forbidden
import io.ktor.http.HttpStatusCode.Companion.NotFound import io.ktor.http.HttpStatusCode.Companion.NotFound
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import io.ktor.http.HttpStatusCode.Companion.Unauthorized
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
@@ -44,7 +41,6 @@ class `Article routes` : BaseTest() {
`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 not contain`("$.result[1]") `And the response should not contain`("$.result[1]")
`And the response should contain pattern`("$.result[0].createdAt", """[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z""") // 2021-04-16T16:39:06.890Z
`And the response should contain list`("$.result", 1) `And the response should contain list`("$.result", 1)
} }
} }
@@ -83,7 +79,6 @@ class `Article routes` : BaseTest() {
`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") `which contains` "65cda9f3-8991-4420-8d41-1da9da72c9bb" `And have property`("$.id") `which contains` "65cda9f3-8991-4420-8d41-1da9da72c9bb"
`And the response should contain pattern`("$.createdAt", """[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z""") // 2021-04-16T16:39:06.890Z
} }
} }
} }
@@ -99,47 +94,6 @@ class `Article routes` : BaseTest() {
} }
} }
@Test
@Tag("draft")
fun `I can get my draft article by id`() {
withIntegrationApplication {
`Given I have citizen`("Neil", "Armstrong")
`Given I have draft article`(id = "d946e16f-ca42-4cf9-a711-a0f8cae60a55", createdBy = Name("Neil", "Armstrong"))
`When I send a GET request`("/articles/d946e16f-ca42-4cf9-a711-a0f8cae60a55") {
`authenticated as`("Neil", "Armstrong")
} `Then the response should be` OK and {
`And the response should not be null`()
`And have property`("$.id") `which contains` "d946e16f-ca42-4cf9-a711-a0f8cae60a55"
}
}
}
@Test
@Tag("draft")
fun `I cannot get draft article by id if not owner`() {
withIntegrationApplication {
`Given I have citizen`("Thomas", "Pesquet")
`Given I have citizen`("Youri", "Gagarine")
`Given I have draft article`(id = "bf13c84c-609f-49b9-9d1d-e2e9655ed8ad")
`When I send a GET request`("/articles/bf13c84c-609f-49b9-9d1d-e2e9655ed8ad") {
`authenticated as`("Youri", "Gagarine")
} `Then the response should be` Forbidden and {
`And the response should not be null`()
}
}
}
@Test
@Tag("draft")
fun `I cannot get draft article by id if not connected`() {
withIntegrationApplication {
`Given I have draft article`(id = "bf13c84c-609f-49b9-9d1d-e2e9655ed8ad")
`When I send a GET request`("/articles/bf13c84c-609f-49b9-9d1d-e2e9655ed8ad") `Then the response should be` Unauthorized and {
`And the response should not be null`()
}
}
}
@Test @Test
@Tag("BadRequest") @Tag("BadRequest")
fun `I cannot get article by id with wrong id format`() { fun `I cannot get article by id with wrong id format`() {

View File

@@ -10,6 +10,7 @@ import fr.postgresjson.migration.Migrations
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.server.testing.TestApplicationEngine import io.ktor.server.testing.TestApplicationEngine
import io.ktor.server.testing.createTestEnvironment import io.ktor.server.testing.createTestEnvironment
import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.lettuce.core.api.sync.RedisCommands import io.lettuce.core.api.sync.RedisCommands
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -40,6 +41,7 @@ abstract class BaseTest : KoinTest {
} }
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@BeforeAll @BeforeAll
fun before() { fun before() {

View File

@@ -1,16 +1,16 @@
package integration package integration
import integration.steps.`when`.Validate
import integration.steps.`when`.`When I send a GET 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 citizen`
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 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.`which contains` import integration.steps.then.`which contains`
import integration.steps.`when`.Validate import integration.steps.then.and
import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body`
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
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,6 +1,12 @@
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 POST 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`
@@ -9,12 +15,6 @@ 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 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 POST request`
import integration.steps.`when`.`with body`
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
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,21 +1,21 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have comment on constitution`
import integration.steps.given.`Given I have constitution`
import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be`
import integration.steps.then.and
import integration.steps.`when`.Validate import integration.steps.`when`.Validate
import integration.steps.`when`.Validate.ALL import integration.steps.`when`.Validate.ALL
import integration.steps.`when`.Validate.REQUEST_BODY 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`
import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have comment on constitution`
import integration.steps.given.`Given I have constitution`
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.`And the response should not be null`
import integration.steps.then.`Then the response should be`
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
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,6 +1,12 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI 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 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`
@@ -10,12 +16,6 @@ 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 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 POST request`
import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body`
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
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,5 +1,11 @@
package integration package integration
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 POST request`
import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have constitution` import integration.steps.given.`Given I have constitution`
import integration.steps.given.`Given I have constitutions` import integration.steps.given.`Given I have constitutions`
@@ -8,14 +14,8 @@ import integration.steps.then.`And have property`
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.`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.`which contains` import integration.steps.then.`which contains`
import integration.steps.`when`.Validate.ALL import integration.steps.then.and
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 POST request`
import integration.steps.`when`.`with body`
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
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,5 +1,8 @@
package integration package integration
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 POST request`
import integration.steps.given.`And follow article` import integration.steps.given.`And follow article`
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`
@@ -10,9 +13,6 @@ 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 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 POST request`
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.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,96 +0,0 @@
package integration
import integration.steps.given.`And follow citizen`
import integration.steps.given.`Given I have citizen`
import integration.steps.given.`authenticated as`
import integration.steps.given.`with no content`
import integration.steps.then.`And the response should be null`
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.and
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 POST request`
import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.NoContent
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("article"), Tag("follow"))
class `Follow citizen routes` : BaseTest() {
@Test
fun `I can follow citizen`() {
withIntegrationApplication {
/* Followed user */
`Given I have citizen`("John", "Glenn", id = "7e1580c5-05b7-4557-84f4-faac9f0a9441")
/* Current user */
`Given I have citizen`("Valentina", "Terechkova")
`When I send a POST request`("/citizens/7e1580c5-05b7-4557-84f4-faac9f0a9441/follows") {
`authenticated as`("Valentina", "Terechkova")
`with no content`()
} `Then the response should be` Created
}
}
@Test
fun `I can get my follow citizen`() {
withIntegrationApplication {
/* Followed user */
`Given I have citizen`("Jean-Loup", "Chrétien", id = "c2432b94-a509-4116-a8b6-9774bc963372")
/* Current user */
`Given I have citizen`("John", "Young", id = "6d41ce65-9df7-47e0-af46-8da4a909490b") {
`And follow citizen`("c2432b94-a509-4116-a8b6-9774bc963372")
}
/* Get my all follows */
`When I send a GET request`("/citizens/6d41ce65-9df7-47e0-af46-8da4a909490b/follows/citizens") {
`authenticated as`("John", "Young")
} `Then the response should be` OK and {
`And the response should not be null`()
`And the response should contain`("$.currentPage", 1)
`And the response should contain`("$.limit", 50)
}
}
}
@Test
fun `I can unfollow citizen`() {
withIntegrationApplication {
/* Followed user */
`Given I have citizen`("Bruce", "McCandless", id = "680c7af7-d2de-4249-bfcb-47007ef546fe")
/* Current user */
`Given I have citizen`("Jean-François", "Clervoy", id = "a12455ae-1047-43ff-826d-0d826dbe90f7") {
`And follow citizen`("680c7af7-d2de-4249-bfcb-47007ef546fe")
}
`When I send a DELETE request`("/citizens/680c7af7-d2de-4249-bfcb-47007ef546fe/follows") {
`authenticated as`("Jean-François", "Clervoy")
`with no content`()
} `Then the response should be` NoContent and {
`And the response should be null`()
}
}
}
@Test
fun `I can know if I follow an citizen`() {
withIntegrationApplication {
/* Followed user */
`Given I have citizen`("Eugene", "Cernan", id = "c755788f-7f48-4cde-8ff0-e75bcffdafc2")
/* Current user */
`Given I have citizen`("Buzz", "Aldrin", id = "39e2915a-e96f-43ea-babd-bd339d8bf197") {
`And follow citizen`("c755788f-7f48-4cde-8ff0-e75bcffdafc2")
}
`When I send a GET request`("/citizens/c755788f-7f48-4cde-8ff0-e75bcffdafc2/follows") {
`authenticated as`("Buzz", "Aldrin")
`with no content`()
} `Then the response should be` OK and {
`And the response should not be null`()
`And the response should contain`("$.target.id", "c755788f-7f48-4cde-8ff0-e75bcffdafc2")
}
}
}
}

View File

@@ -1,5 +1,8 @@
package integration package integration
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 POST request`
import integration.steps.given.`And follow constitution` import integration.steps.given.`And follow constitution`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have constitution` import integration.steps.given.`Given I have constitution`
@@ -10,9 +13,6 @@ 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 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 POST request`
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.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,13 +1,13 @@
package integration package integration
import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
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 should contains` import integration.steps.then.`and should contains`
import integration.steps.`when`.`When I send a POST request` import integration.steps.then.and
import integration.steps.`when`.`with body`
import io.ktor.http.HttpStatusCode.Companion.NoContent import io.ktor.http.HttpStatusCode.Companion.NoContent
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

View File

@@ -1,20 +1,25 @@
package integration 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.citizen.database.CitizenI.Name
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.NotificationMessage import fr.dcproject.component.notification.Notification
import integration.steps.given.`And follow citizen` import fr.dcproject.component.notification.Publisher
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have article update notification`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have follow on article` import integration.steps.given.`Given I have follow on article`
import integration.steps.given.`authenticated in url as` import integration.steps.given.`authenticated in url as`
import io.ktor.http.cio.websocket.Frame import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText import io.ktor.http.cio.websocket.readText
import kotlinx.coroutines.launch
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.TestInstance import org.junit.jupiter.api.TestInstance
import org.koin.test.get
import kotlin.test.assertEquals import kotlin.test.assertEquals
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -26,7 +31,26 @@ class `Notification routes` : BaseTest() {
`Given I have citizen`("John", "Doe", id = "1a34191a-9cde-45ba-8ac1-230138a102d3") `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 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") `Given I have follow on article`("John", "Doe", article = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4")
`Given I have article update notification`("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) Thread.sleep(1000)
@@ -38,41 +62,7 @@ class `Notification routes` : BaseTest() {
) { incoming, outgoing -> ) { incoming, outgoing ->
incoming.receive().let { incoming.receive().let {
when (it) { when (it) {
is Frame.Text -> NotificationMessage.fromString<ArticleUpdateNotificationMessage>(it.readText()).let { notif -> 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())
}
}
}
}
}
@Test
fun `I can receive article update notification when follow the creator`() {
withIntegrationApplication {
`Given I have citizen`("Thomas", "Pesquet", id = "1a34191a-9cde-45ba-8ac1-230138a102d3")
`Given I have article`(id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", createdBy = Name(firstName = "Thomas", lastName = "Pesquet"))
`Given I have citizen`("Alan", "Bean") {
`And follow citizen`(Name("Thomas", "Pesquet"))
}
`Given I have article update notification`("a06cbfb7-3094-4d64-aaa1-7486c0c292f4")
Thread.sleep(1000)
handleWebSocketConversation(
"/notifications",
{
`authenticated in url as`("Alan", "Bean")
}
) { incoming, outgoing ->
incoming.receive().let {
when (it) {
is Frame.Text -> NotificationMessage.fromString<ArticleUpdateNotificationMessage>(it.readText()).let { notif ->
assertEquals( assertEquals(
"a06cbfb7-3094-4d64-aaa1-7486c0c292f4", "a06cbfb7-3094-4d64-aaa1-7486c0c292f4",
notif.target.id.toString() notif.target.id.toString()

View File

@@ -1,22 +1,18 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have an opinion choice` import integration.steps.given.`Given I have an opinion choice`
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 opinion on article` 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`
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 not be null` import integration.steps.then.`And the response should contain`
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 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 PUT request`
import integration.steps.`when`.`with body`
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
@@ -137,7 +133,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?page=1&limit=10") { `When I send a GET request`("/citizens/c1542096-3431-432d-8e35-9dc071d4c818/opinions/articles") {
`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")
@@ -145,26 +141,4 @@ 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

@@ -1,13 +1,13 @@
package integration package integration
import integration.steps.`when`.Validate
import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body`
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 pattern` import integration.steps.then.`And the response should contain pattern`
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 integration.steps.`when`.Validate
import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body`
import io.ktor.http.HttpStatusCode.Companion.BadRequest import io.ktor.http.HttpStatusCode.Companion.BadRequest
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

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`.`When I send a GET 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`
@@ -9,141 +12,53 @@ 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 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 PUT request`
import integration.steps.`when`.`with body`
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() {
@TestFactory @Test
fun `I can vote article`(): List<DynamicTest> { fun `I can vote article`() {
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": $note "note": 1
} }
""" """
) )
} `Then the response should be` Created } `Then the response should be` Created
} }
} }
}
}
@TestFactory @Test
@Tag("BadRequest") fun `I can vote constitution`() {
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": $note "note": 1
}
""" """
) )
} `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`() {
@@ -151,7 +66,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?page=1&limit=50") { `When I send a GET request`("/citizens/c044823d-e778-4256-9016-b1334bf933d3/votes/articles") {
`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)
@@ -162,23 +77,6 @@ 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 {
@@ -220,39 +118,4 @@ 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

@@ -1,118 +0,0 @@
package integration
import fr.dcproject.component.citizen.database.CitizenI.Name
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`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`Then the response should be`
import integration.steps.then.and
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 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,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`.`When I send a DELETE 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 citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have workgroup` import integration.steps.given.`Given I have workgroup`
import integration.steps.given.`With members` import integration.steps.given.`With members`
@@ -8,19 +13,10 @@ import integration.steps.given.`authenticated as`
import integration.steps.given.`with no content` import integration.steps.given.`with no content`
import integration.steps.then.`And have property` 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`
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 not be null` import integration.steps.then.`And the response should contain`
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 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 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 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
@@ -77,7 +73,7 @@ class `Workgroup routes` : BaseTest() {
{ {
"id":"f496d86d-6654-4068-91ff-90e1dbcc5f38", "id":"f496d86d-6654-4068-91ff-90e1dbcc5f38",
"name":"Les Bouffons", "name":"Les Bouffons",
"description":"Pellentesque eleifend malesuada aliquam. Maecenas et urna quis nunc lacinia scelerisque.", "description":"La vie est belle",
"anonymous":false "anonymous":false
} }
""" """
@@ -85,7 +81,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", "Pellentesque eleifend malesuada aliquam. Maecenas et urna quis nunc lacinia scelerisque.") `And the response should contain`("$.description", "La vie est belle")
`And the response should contain`("$.anonymous", false) `And the response should contain`("$.anonymous", false)
} }
@@ -95,36 +91,6 @@ 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 {
@@ -143,15 +109,14 @@ class `Workgroup routes` : BaseTest() {
""" """
{ {
"name":"La ratatouille", "name":"La ratatouille",
"description":"Une petite souris avec un chapeau et qui aime la cuisine", "description":"Une petite souris"
"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 avec un chapeau et qui aime la cuisine") `And the response should contain`("$.description", "Une petite souris")
`And have property`("$.members") `And have property`("$.members")
`And the response should contain list`("$.members", 3) `And the response should contain list`("$.members", 3)
@@ -164,43 +129,7 @@ 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 avec un chapeau et qui aime la cuisine") `And the response should contain`("$.description", "Une petite souris")
}
}
}
@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")
} }
} }
} }
@@ -228,7 +157,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?page=1&limit=10&sort=createdAt") { `When I send a GET request`("/workgroups") {
`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 {
@@ -238,15 +167,94 @@ class `Workgroup routes` : BaseTest() {
} }
@Test @Test
@Tag("BadRequest") fun `I can add member to workgroup`() {
fun `I cannot get workgroups list with wrong request`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have workgroup`("3fd8edb6-c4b4-4c94-bc75-ddd9b290d32c") `Given I have citizen`("Blaise", "Pascal")
`When I send a GET request`("/workgroups?sort=plop", -REQUEST_PARAM) { `Given I have citizen`("Roger", "Penrose", id = "6d883fe7-5fc0-4a50-8858-72230673eba4")
} `Then the response should be` BadRequest and { `Given I have citizen`("Alessandro", "Volta", id = "b5bac515-45d4-4aeb-9b6d-2627a0bbc419")
`And the response should not be null`() `Given I have workgroup`("b0ea1922-3bc6-44e2-aa7c-40158998cfbb", createdBy = Name("Blaise", "Pascal"))
`And the response should contain`("$.invalidParams[0].name", ".sort") `When I send a POST request`("/workgroups/b0ea1922-3bc6-44e2-aa7c-40158998cfbb/members") {
`And the response should contain`("$.invalidParams[0].reason", "must be one of: 'name', 'createdAt'") `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

@@ -20,14 +20,6 @@ fun TestApplicationEngine.`Given I have article`(
createArticle(id?.toUUID(), workgroup, createCitizen(name = createdBy)) createArticle(id?.toUUID(), workgroup, createCitizen(name = createdBy))
} }
fun TestApplicationEngine.`Given I have draft article`(
id: String? = null,
workgroup: WorkgroupRef? = null,
createdBy: Name? = null
) {
createArticle(id?.toUUID(), workgroup, createCitizen(name = createdBy), draft = true)
}
fun TestApplicationEngine.`Given I have article`( fun TestApplicationEngine.`Given I have article`(
id: String? = null, id: String? = null,
workgroup: WorkgroupRef? = null, workgroup: WorkgroupRef? = null,
@@ -52,10 +44,9 @@ 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: CitizenRef = createCitizen(), createdBy: CitizenRef = createCitizen()
draft: Boolean = false,
): ArticleForView { ): ArticleForView {
val articleRepository: ArticleRepository by lazy { GlobalContext.get().get() } val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() }
val article = ArticleForUpdate( val article = ArticleForUpdate(
id = id ?: UUID.randomUUID(), id = id ?: UUID.randomUUID(),
@@ -64,8 +55,7 @@ fun createArticle(
description = LoremIpsum().getParagraphs(1, 2), description = LoremIpsum().getParagraphs(1, 2),
createdBy = createdBy, createdBy = createdBy,
workgroup = workgroup, workgroup = workgroup,
versionId = UUID.randomUUID(), versionId = UUID.randomUUID()
draft = draft,
) )
return articleRepository.upsert(article) ?: error("Cannot create article") return articleRepository.upsert(article) ?: error("Cannot create article")
} }

View File

@@ -14,9 +14,9 @@ fun TestApplicationRequest.`authenticated as`(
lastName: String, lastName: String,
): Citizen { ): Citizen {
val username = "$firstName-$lastName".toLowerCase() val username = "$firstName-$lastName".toLowerCase()
val repo: CitizenRepository by lazy<CitizenRepository> { GlobalContext.get().get() } val repo: CitizenRepository by lazy<CitizenRepository> { GlobalContext.get().koin.get() }
val citizen = repo.findByUsername(username) ?: error("Citizen not exist with username $username") val citizen = repo.findByUsername(username) ?: error("Citizen not exist with username $username")
val algorithm = GlobalContext.get().get<JwtConfig>().algorithm val algorithm = GlobalContext.get().koin.get<JwtConfig>().algorithm
val jwtAsString: String = JWT.create() val jwtAsString: String = JWT.create()
.withIssuer("dc-project.fr") .withIssuer("dc-project.fr")
.withClaim("id", citizen.user.id.toString()) .withClaim("id", citizen.user.id.toString())
@@ -30,9 +30,9 @@ fun TestApplicationRequest.`authenticated in url as`(
firstName: String, firstName: String,
lastName: String, lastName: String,
): Citizen { ): Citizen {
val repo: CitizenRepository by lazy<CitizenRepository> { GlobalContext.get().get() } 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 citizen = repo.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist with name $firstName $lastName")
val algorithm = GlobalContext.get().get<JwtConfig>().algorithm val algorithm = GlobalContext.get().koin.get<JwtConfig>().algorithm
val jwtAsString: String = JWT.create() val jwtAsString: String = JWT.create()
.withIssuer("dc-project.fr") .withIssuer("dc-project.fr")
.withClaim("id", citizen.user.id.toString()) .withClaim("id", citizen.user.id.toString())

View File

@@ -18,7 +18,7 @@ fun TestApplicationEngine.`Given I have citizen`(
id: String = UUID.randomUUID().toString(), id: String = UUID.randomUUID().toString(),
callback: Citizen.() -> Unit = {} callback: Citizen.() -> Unit = {}
): Citizen? { ): Citizen? {
val repo: CitizenRepository by lazy { GlobalContext.get().get() } val repo: CitizenRepository by lazy { GlobalContext.get().koin.get() }
val user = UserForCreate( val user = UserForCreate(
id = id.toUUID(), id = id.toUUID(),
@@ -37,7 +37,7 @@ fun TestApplicationEngine.`Given I have citizen`(
} }
fun createCitizen(name: CitizenI.Name? = null, id: UUID = UUID.randomUUID()): Citizen { fun createCitizen(name: CitizenI.Name? = null, id: UUID = UUID.randomUUID()): Citizen {
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() } val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
return if (name != null) { return if (name != null) {
citizenRepository.findByName(name) ?: error("Citizen not exist") citizenRepository.findByName(name) ?: error("Citizen not exist")

View File

@@ -43,7 +43,7 @@ fun <A : ArticleRef> createComment(
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
): CommentForView<TargetRef, CitizenCreator> { ): CommentForView<TargetRef, CitizenCreator> {
val articleRepository: ArticleRepository by lazy { GlobalContext.get().get() } val articleRepository: ArticleRepository by lazy { GlobalContext.get().koin.get() }
return 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),
@@ -67,7 +67,7 @@ fun <C : ConstitutionRef> createComment(
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
): CommentForView<TargetRef, CitizenCreator> { ): CommentForView<TargetRef, CitizenCreator> {
val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().get() } val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().koin.get() }
return 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),
@@ -82,7 +82,7 @@ fun <T : TargetI> createCommentOnTarget(
createdBy: Name? = null, createdBy: Name? = null,
content: String? = null content: String? = null
): CommentForView<TargetRef, CitizenCreator> { ): CommentForView<TargetRef, CitizenCreator> {
val commentRepository: CommentRepository by lazy { GlobalContext.get().get() } val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() }
val creator = createCitizen(createdBy) val creator = createCitizen(createdBy)
val comment = CommentForUpdate( val comment = CommentForUpdate(
id = id ?: UUID.randomUUID(), id = id ?: UUID.randomUUID(),
@@ -114,7 +114,7 @@ fun createCommentOnComment(
content: String? = null content: String? = null
): CommentForView<out TargetRef, CitizenCreator> { ): CommentForView<out TargetRef, CitizenCreator> {
val creator = createCitizen(createdBy) val creator = createCitizen(createdBy)
val commentRepository: CommentRepository by lazy { GlobalContext.get().get() } val commentRepository: CommentRepository by lazy { GlobalContext.get().koin.get() }
val parentComment = if (parent == null) { val parentComment = if (parent == null) {
createComment<ArticleRef>() createComment<ArticleRef>()
} else { } else {

View File

@@ -44,7 +44,7 @@ fun createConstitution(
titles: List<TitleForUpdate<ArticleRef>>? = null, titles: List<TitleForUpdate<ArticleRef>>? = null,
createdBy: Name? = null createdBy: Name? = null
): ConstitutionForView { ): ConstitutionForView {
val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().get() } val constitutionRepository: ConstitutionRepository by lazy { GlobalContext.get().koin.get() }
val creator: CitizenWithUserI = createCitizen(createdBy) val creator: CitizenWithUserI = createCitizen(createdBy)

View File

@@ -8,7 +8,6 @@ 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
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowCitizenRepository
import fr.dcproject.component.follow.database.FollowConstitutionRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository
import fr.dcproject.component.follow.database.FollowForUpdate import fr.dcproject.component.follow.database.FollowForUpdate
import io.ktor.server.testing.TestApplicationEngine import io.ktor.server.testing.TestApplicationEngine
@@ -25,64 +24,35 @@ fun Citizen.`And follow constitution`(
) { ) {
createFollow(this, ConstitutionRef(constitution.toUUID())) createFollow(this, ConstitutionRef(constitution.toUUID()))
} }
fun Citizen.`And follow citizen`(
citizen: String,
) {
createFollow(this, CitizenRef(citizen.toUUID()))
}
fun Citizen.`And follow citizen`(
name: CitizenI.Name,
) {
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() }
val citizen = citizenRepository.findByName(name) ?: error("Citizen not exist")
createFollow(this, CitizenRef(citizen.id))
}
fun TestApplicationEngine.`Given I have follow on article`( fun TestApplicationEngine.`Given I have follow on article`(
firstName: String, firstName: String,
lastName: String, lastName: String,
article: String, article: String,
) { ) {
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() } val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: 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()))
} }
fun TestApplicationEngine.`Given I have follow on citizen`(
firstName: String,
lastName: String,
target: CitizenI.Name,
) {
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() }
val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist")
val targetCitizen = citizenRepository.findByName(target) ?: error("Citizen not exist")
createFollow(citizen, CitizenRef(targetCitizen.id))
}
fun TestApplicationEngine.`Given I have follow on constitution`( fun TestApplicationEngine.`Given I have follow on constitution`(
firstName: String, firstName: String,
lastName: String, lastName: String,
constitution: String, constitution: String,
) { ) {
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() } val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: 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()))
} }
fun createFollow(citizen: CitizenRef, article: ArticleRef) { fun createFollow(citizen: CitizenRef, article: ArticleRef) {
val followArticleRepository: FollowArticleRepository by lazy { GlobalContext.get().get() } val followArticleRepository: FollowArticleRepository by lazy { GlobalContext.get().koin.get() }
val follow = FollowForUpdate(createdBy = citizen, target = article) val follow = FollowForUpdate(createdBy = citizen, target = article)
followArticleRepository.follow(follow) followArticleRepository.follow(follow)
} }
fun createFollow(citizen: CitizenRef, constitution: ConstitutionRef) { fun createFollow(citizen: CitizenRef, constitution: ConstitutionRef) {
val followConstitutionRepository: FollowConstitutionRepository by lazy { GlobalContext.get().get() } val followConstitutionRepository: FollowConstitutionRepository by lazy { GlobalContext.get().koin.get() }
val follow = FollowForUpdate(createdBy = citizen, target = constitution) val follow = FollowForUpdate(createdBy = citizen, target = constitution)
followConstitutionRepository.follow(follow) followConstitutionRepository.follow(follow)
} }
fun createFollow(createdBy: CitizenRef, target: CitizenRef) {
val followCitizenRepository: FollowCitizenRepository by lazy { GlobalContext.get().get() }
val follow = FollowForUpdate(createdBy = createdBy, target = target)
followCitizenRepository.follow(follow)
}

Some files were not shown because too many files have changed in this diff Show More