Compare commits
73 Commits
refactorin
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
| 32f62ec5ab | |||
| 39c665b7a9 | |||
| 50b4cf1816 | |||
| 6a5e00bb4d | |||
| 0c8bcbd634 | |||
| 8223dd21bb | |||
| 27e405c585 | |||
| 34513e25b6 | |||
| f5c1aa29e8 | |||
| 875d0bfffa | |||
| fb7b07340a | |||
| a07b19a3cb | |||
| 13cdaaf01a | |||
| e473e62068 | |||
| 9d3eeeb04b | |||
| eb399392c9 | |||
| 1ec1c59c8c | |||
| 9511331cd2 | |||
| 33a8cdb169 | |||
| 6aa3ddb28d | |||
| 708d241a26 | |||
| e4745e71c2 | |||
| e26710898e | |||
| fe11384ad2 | |||
| 61a7091736 | |||
| 2ef9f65f2c | |||
| b5fc3d25bb | |||
| 3faf2e5f0d | |||
| ab418ae300 | |||
| 395d64a44a | |||
| a300e275d4 | |||
| 3a18ef0554 | |||
| 921a545877 | |||
| ef942b956e | |||
| ff74ad7e47 | |||
| 2bb90ced03 | |||
| a48cd52652 | |||
| dd4c2dadab | |||
| c81b63aef2 | |||
| cb762a446a | |||
| db810ab0c6 | |||
| 01c5b78325 | |||
| 1bc7293660 | |||
| 55c890aca5 | |||
| c0e364637a | |||
| 0a1ed9ba82 | |||
| 620085fda8 | |||
| 3b5c1cf68a | |||
| a0d07e88a1 | |||
| f17277c0e9 | |||
| 9f13213a35 | |||
| 5f0b8de159 | |||
| 6b66130ddc | |||
| 7f93ec5044 | |||
| 1be608e6b2 | |||
| b13cd5544c | |||
| 104f0fb3fc | |||
| b2f40ff421 | |||
| 09e81620a1 | |||
| 7e16c7bb74 | |||
| fe953fc967 | |||
| 453fd2225c | |||
| 70fd54d831 | |||
| dcf7a2bc06 | |||
| 118af0170a | |||
| 0aa8089a9a | |||
| fef5f3b396 | |||
| 1838b90ac9 | |||
| 73fa2be91f | |||
| 52183abd08 | |||
| e19266d4cc | |||
| f458d7b674 | |||
| 29d4d6ec25 |
134
.github/workflows/tests.yml
vendored
Normal file
134
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
# This workflow will build a Java project with Gradle
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
|
||||
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Build
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
gradle-version: 6.8
|
||||
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
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
gradle-version: 6.8
|
||||
arguments: processResources
|
||||
- name: processTestResources
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
gradle-version: 6.8
|
||||
arguments: processResources
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Build
|
||||
path: build
|
||||
|
||||
testSql:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Build
|
||||
path: build
|
||||
- name: TestSql
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
gradle-version: 6.8
|
||||
arguments: testSql
|
||||
|
||||
test:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Build
|
||||
path: build
|
||||
- name: Test
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
gradle-version: 6.8
|
||||
arguments: test -x testSql
|
||||
- name: Coverage
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
gradle-version: 6.8
|
||||
arguments: coveralls
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
- name: Build and analyze
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: ./gradlew build sonarqube --info
|
||||
|
||||
lint:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Build
|
||||
path: build
|
||||
- name: Lint
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
gradle-version: 6.8
|
||||
arguments: ktlintCheck
|
||||
2
.idea/dataSources.xml
generated
2
.idea/dataSources.xml
generated
@@ -11,7 +11,7 @@
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://localhost:5433/test</jdbc-url>
|
||||
<jdbc-url>jdbc:postgresql://localhost:15432/test</jdbc-url>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="sonar@localhost" uuid="ee78beab-120d-4740-ad21-d4d9e2121d25">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
|
||||
5
.idea/runConfigurations/Sonarqube.xml
generated
5
.idea/runConfigurations/Sonarqube.xml
generated
@@ -1,6 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Sonarqube" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<entry key="SONAR_TOKEN" value="15ad34f46763706727d884ced12c48d5222fe639" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
|
||||
10
README.md
10
README.md
@@ -1,6 +1,14 @@
|
||||
# DC Project
|
||||
|
||||
[Installation](./doc/installation)
|
||||
[](https://sonarcloud.io/dashboard?id=dc-project)
|
||||
|
||||
[](https://github.com/flecomte/dc-project/actions/workflows/tests.yml)
|
||||
[](https://coveralls.io/github/flecomte/dc-project?branch=master)
|
||||
[](https://sonarcloud.io/dashboard?id=dc-project)
|
||||
|
||||
[](https://sonarcloud.io/dashboard?id=dc-project)
|
||||
|
||||
[Installation](./doc/installation/Installation.md)
|
||||
|
||||
### Run dockers
|
||||
```bash
|
||||
|
||||
130
build.gradle.kts
130
build.gradle.kts
@@ -41,6 +41,7 @@ plugins {
|
||||
id("net.nemerosa.versioning") version "2.14.0"
|
||||
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
|
||||
id("com.avast.gradle.docker-compose") version "0.14.0"
|
||||
id("com.github.kt3k.coveralls") version "2.8.4"
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -56,10 +57,13 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.typesafe:config:1.4.1")
|
||||
classpath("com.github.flecomte:postgres-json:2.1.1")
|
||||
classpath("com.github.flecomte:postgres-json:2.1.2")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.distZip.configure { enabled = false }
|
||||
tasks.distTar.configure { enabled = false }
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
@@ -71,7 +75,7 @@ val migration by tasks.registering {
|
||||
dependsOn(tasks.named("composeUp"))
|
||||
|
||||
doLast {
|
||||
val config = ConfigFactory.parseFile(file("$buildDir/../src/main/resources/application.conf")).resolve()
|
||||
val config = ConfigFactory.parseFile(file("$buildDir/resources/main/application.conf")).resolve()
|
||||
val connection = Connection(
|
||||
host = config.getString("db.host"),
|
||||
port = config.getInt("db.port"),
|
||||
@@ -81,8 +85,8 @@ val migration by tasks.registering {
|
||||
)
|
||||
Migrations(
|
||||
connection,
|
||||
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
|
||||
file("$buildDir/../src/main/resources/sql/functions").toURI()
|
||||
file("$buildDir/resources/main/sql/migrations").toURI(),
|
||||
file("$buildDir/resources/main/sql/functions").toURI()
|
||||
).run {
|
||||
run()
|
||||
}
|
||||
@@ -94,7 +98,7 @@ val migrationTest by tasks.registering {
|
||||
dependsOn(tasks.named("testComposeUp"))
|
||||
finalizedBy(tasks.named("testComposeDown"))
|
||||
doLast {
|
||||
val config = ConfigFactory.parseFile(file("$buildDir/../src/test/resources/application-test.conf")).resolve()
|
||||
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
|
||||
val connection = Connection(
|
||||
host = config.getString("db.host"),
|
||||
port = config.getInt("db.port"),
|
||||
@@ -104,8 +108,8 @@ val migrationTest by tasks.registering {
|
||||
)
|
||||
Migrations(
|
||||
connection,
|
||||
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
|
||||
file("$buildDir/../src/main/resources/sql/functions").toURI()
|
||||
file("$buildDir/resources/main/sql/migrations").toURI(),
|
||||
file("$buildDir/resources/main/sql/functions").toURI()
|
||||
).run {
|
||||
run()
|
||||
connection.disconnect()
|
||||
@@ -115,11 +119,13 @@ val migrationTest by tasks.registering {
|
||||
|
||||
val testSql by tasks.registering {
|
||||
group = "verification"
|
||||
dependsOn(tasks.named("testComposeUp"))
|
||||
finalizedBy(tasks.named("testComposeDown"))
|
||||
dependsOn(tasks.named("processResources"))
|
||||
dependsOn(tasks.named("processTestResources"))
|
||||
dependsOn(tasks.named("testSqlComposeUp"))
|
||||
finalizedBy(tasks.named("testSqlComposeDown"))
|
||||
|
||||
doLast {
|
||||
val config = ConfigFactory.parseFile(file("$buildDir/../src/test/resources/application-test.conf")).resolve()
|
||||
val config = ConfigFactory.parseFile(file("$buildDir/resources/test/application-test.conf")).resolve()
|
||||
|
||||
val connection = Connection(
|
||||
host = config.getString("db.host"),
|
||||
@@ -131,16 +137,14 @@ val testSql by tasks.registering {
|
||||
|
||||
Migrations(
|
||||
connection,
|
||||
file("$buildDir/../src/main/resources/sql/migrations").toURI(),
|
||||
file("$buildDir/../src/main/resources/sql/functions").toURI(),
|
||||
file("$buildDir/../src/test/sql/fixtures").toURI()
|
||||
).run {
|
||||
run()
|
||||
}
|
||||
file("$buildDir/resources/main/sql/migrations").toURI(),
|
||||
file("$buildDir/resources/main/sql/functions").toURI(),
|
||||
file("$buildDir/resources/test/sql/fixtures").toURI()
|
||||
).run()
|
||||
|
||||
Requester.RequesterFactory(
|
||||
connection = connection,
|
||||
queriesDirectory = file("$buildDir/../src/test/sql").toURI()
|
||||
queriesDirectory = file("$buildDir/resources/test/sql").toURI()
|
||||
).createRequester().run {
|
||||
getQueries().map {
|
||||
try {
|
||||
@@ -178,7 +182,11 @@ tasks.named<ShadowJar>("shadowJar") {
|
||||
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}")
|
||||
}
|
||||
|
||||
tasks.sonarqube.configure { dependsOn(tasks.jacocoTestReport) }
|
||||
tasks.sonarqube.configure {
|
||||
dependsOn(tasks.test)
|
||||
dependsOn(tasks.detekt)
|
||||
dependsOn(tasks.jacocoTestReport)
|
||||
}
|
||||
|
||||
val sourcesJar by tasks.registering(Jar::class) {
|
||||
group = "build"
|
||||
@@ -189,11 +197,22 @@ val sourcesJar by tasks.registering(Jar::class) {
|
||||
tasks.test {
|
||||
useJUnit()
|
||||
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
|
||||
}
|
||||
|
||||
coveralls {
|
||||
sourceDirs.add("src/main/kotlin")
|
||||
}
|
||||
|
||||
tasks.register("testAll") {
|
||||
group = "verification"
|
||||
dependsOn(testSql)
|
||||
dependsOn(tasks.test)
|
||||
dependsOn(tasks.ktlintCheck)
|
||||
}
|
||||
|
||||
apply(plugin = "docker-compose")
|
||||
dockerCompose {
|
||||
projectName = "dc-project"
|
||||
@@ -203,23 +222,22 @@ dockerCompose {
|
||||
removeVolumes = false
|
||||
removeContainers = false
|
||||
isRequiredBy(project.tasks.run)
|
||||
|
||||
createNested("testSql").apply {
|
||||
projectName = "dc-project_test"
|
||||
useComposeFiles = listOf("docker-compose-test.yml")
|
||||
startedServices = listOf("db", "elasticsearch")
|
||||
stopContainers = false
|
||||
isRequiredBy(project.tasks.named("testSql"))
|
||||
}
|
||||
|
||||
createNested("test").apply {
|
||||
projectName = "dc-project_test"
|
||||
useComposeFiles = listOf("docker-compose-test.yml")
|
||||
stopContainers = false
|
||||
isRequiredBy(project.tasks.test)
|
||||
isRequiredBy(project.tasks.named("testSql"))
|
||||
}
|
||||
createNested("sonarqube").apply {
|
||||
projectName = "dc-project"
|
||||
useComposeFiles = listOf("docker-compose-sonar.yml")
|
||||
stopContainers = false
|
||||
removeVolumes = false
|
||||
removeContainers = false
|
||||
// isRequiredBy(project.tasks.sonarqube)
|
||||
}
|
||||
}
|
||||
tasks.sonarqube.configure { dependsOn(tasks.named("sonarqubeComposeUp")) }
|
||||
|
||||
publishing {
|
||||
if (versioning.info.dirty == false) {
|
||||
@@ -267,6 +285,7 @@ tasks.jacocoTestReport {
|
||||
|
||||
detekt {
|
||||
buildUponDefaultConfig = true // preconfigure defaults
|
||||
ignoreFailures = true
|
||||
// config = files("$projectDir/config/detekt.yml") // point to your custom config defining rules to run, overwriting default behavior
|
||||
// baseline = file("$projectDir/config/baseline.xml") // a way of suppressing issues before introducing detekt
|
||||
|
||||
@@ -281,6 +300,7 @@ detekt {
|
||||
tasks.withType<Detekt> {
|
||||
// Target version of the generated JVM bytecode. It is used for type resolution.
|
||||
this.jvmTarget = "11"
|
||||
ignoreFailures = true
|
||||
}
|
||||
|
||||
val setMaxMapCount = tasks.create<Exec>("setMaxMapCount") {
|
||||
@@ -293,7 +313,49 @@ val setMaxMapCount = tasks.create<Exec>("setMaxMapCount") {
|
||||
}
|
||||
}
|
||||
}
|
||||
tasks.named("testComposeUp").configure { dependsOn(setMaxMapCount) }
|
||||
|
||||
tasks.named("testComposeUp").configure {
|
||||
if (OperatingSystem.current().isWindows) {
|
||||
dependsOn(setMaxMapCount)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("testArticles", Test::class) {
|
||||
group = "tests"
|
||||
useJUnitPlatform {
|
||||
includeTags("article")
|
||||
}
|
||||
}
|
||||
tasks.register("testCitizens", Test::class) {
|
||||
group = "tests"
|
||||
useJUnitPlatform {
|
||||
includeTags("citizen")
|
||||
}
|
||||
}
|
||||
tasks.register("testComments", Test::class) {
|
||||
group = "tests"
|
||||
useJUnitPlatform {
|
||||
includeTags("comment")
|
||||
}
|
||||
}
|
||||
tasks.register("testConstitutions", Test::class) {
|
||||
group = "tests"
|
||||
useJUnitPlatform {
|
||||
includeTags("constitution")
|
||||
}
|
||||
}
|
||||
tasks.register("testFollows", Test::class) {
|
||||
group = "tests"
|
||||
useJUnitPlatform {
|
||||
includeTags("follow")
|
||||
}
|
||||
}
|
||||
tasks.register("testNotifications", Test::class) {
|
||||
group = "tests"
|
||||
useJUnitPlatform {
|
||||
includeTags("notification")
|
||||
}
|
||||
}
|
||||
|
||||
dependencyCheck {
|
||||
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
|
||||
@@ -302,8 +364,9 @@ dependencyCheck {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
maven { url = uri("https://kotlin.bintray.com/ktor") }
|
||||
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 {
|
||||
@@ -327,13 +390,14 @@ dependencies {
|
||||
implementation("net.pearx.kasechange:kasechange-jvm:1.3.0")
|
||||
implementation("com.auth0:java-jwt:3.12.0")
|
||||
implementation("com.github.jasync-sql:jasync-postgresql:1.1.6")
|
||||
implementation("com.github.flecomte:postgres-json:2.1.1")
|
||||
implementation("com.github.flecomte:postgres-json:2.1.2")
|
||||
implementation("com.sendgrid:sendgrid-java:4.7.1")
|
||||
implementation("io.lettuce:lettuce-core:5.3.6.RELEASE") // TODO update to 6.0.2
|
||||
implementation("com.rabbitmq:amqp-client:5.10.0")
|
||||
implementation("org.elasticsearch.client:elasticsearch-rest-client:6.7.1")
|
||||
implementation("com.jayway.jsonpath:json-path:2.5.0")
|
||||
implementation("com.avast.gradle:gradle-docker-compose-plugin:0.14.0")
|
||||
implementation("io.konform:konform-jvm:0.2.0")
|
||||
|
||||
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
|
||||
testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
sonarqube:
|
||||
container_name: ${APP_NAME}_sonarqube
|
||||
image: sonarqube:community
|
||||
depends_on:
|
||||
- sonarqube_db
|
||||
ports:
|
||||
- ${SONARQUBE_PORT}:9000
|
||||
networks:
|
||||
- sonarnet
|
||||
environment:
|
||||
SONAR_JDBC_URL: jdbc:postgresql://sonarqube_db:5432/sonar
|
||||
SONAR_JDBC_USERNAME: sonar
|
||||
SONAR_JDBC_PASSWORD: sonar
|
||||
volumes:
|
||||
- sonarqube_data:/opt/sonarqube/data
|
||||
- sonarqube_extensions:/opt/sonarqube/extensions
|
||||
- sonarqube_logs:/opt/sonarqube/logs
|
||||
- sonarqube_temp:/opt/sonarqube/temp
|
||||
|
||||
sonarqube_db:
|
||||
container_name: ${APP_NAME}_sonarqube_db
|
||||
image: postgres:alpine
|
||||
networks:
|
||||
- sonarnet
|
||||
environment:
|
||||
POSTGRES_USER: sonar
|
||||
POSTGRES_PASSWORD: sonar
|
||||
ports:
|
||||
- ${SONARQUBE_DB_PORT}:5432
|
||||
volumes:
|
||||
- sonarqube_postgresql:/var/lib/postgresql
|
||||
# This needs explicit mapping due to https://github.com/docker-library/postgres/blob/4e48e3228a30763913ece952c611e5e9b95c8759/Dockerfile.template#L52
|
||||
- sonarqube_postgresql_data:/var/lib/postgresql/data
|
||||
|
||||
|
||||
networks:
|
||||
sonarnet:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
sonarqube_data:
|
||||
sonarqube_extensions:
|
||||
sonarqube_logs:
|
||||
sonarqube_temp:
|
||||
sonarqube_postgresql:
|
||||
sonarqube_postgresql_data:
|
||||
@@ -38,6 +38,9 @@ services:
|
||||
REDIS_CONNECTION: ${REDIS_CONNECTION}
|
||||
RABBITMQ_CONNECTION: ${RABBITMQ_CONNECTION}
|
||||
ELASTICSEARCH_CONNECTION: ${ELASTICSEARCH_CONNECTION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_ISSUER: ${JWT_ISSUER}
|
||||
JWT_VALIDITY: ${JWT_VALIDITY}
|
||||
depends_on:
|
||||
- elasticsearch
|
||||
- db
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
kotlin.code.style=official
|
||||
systemProp.sonar.host.url=http://localhost:9002
|
||||
systemProp.sonar.login=admin
|
||||
systemProp.sonar.password=sonar
|
||||
systemProp.sonar.host.url=https://sonarcloud.io
|
||||
systemProp.sonar.projectKey=dc-project
|
||||
systemProp.sonar.projectName=DC Project
|
||||
systemProp.sonar.organization=flecomte
|
||||
systemProp.sonar.java.coveragePlugin=jacoco
|
||||
systemProp.sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
|
||||
systemProp.sonar.kotlin.detekt.reportPaths=build/reports/detekt/detekt.xml
|
||||
|
||||
@@ -6,17 +6,14 @@ import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.datatype.joda.JodaModule
|
||||
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
|
||||
import fr.dcproject.application.Env.PROD
|
||||
import fr.dcproject.application.Env.TEST
|
||||
import fr.dcproject.common.security.AccessDeniedException
|
||||
import fr.dcproject.application.http.statusPagesInstallation
|
||||
import fr.dcproject.component.article.articleKoinModule
|
||||
import fr.dcproject.component.article.routes.installArticleRoutes
|
||||
import fr.dcproject.component.auth.ForbiddenException
|
||||
import fr.dcproject.component.auth.authKoinModule
|
||||
import fr.dcproject.component.auth.jwt.jwtInstallation
|
||||
import fr.dcproject.component.auth.routes.installAuthRoutes
|
||||
import fr.dcproject.component.auth.user
|
||||
import fr.dcproject.component.citizen.citizenKoinModule
|
||||
import fr.dcproject.component.citizen.routes.installCitizenRoutes
|
||||
import fr.dcproject.component.comment.article.routes.installCommentArticleRoutes
|
||||
@@ -41,7 +38,6 @@ import fr.dcproject.component.workgroup.workgroupKoinModule
|
||||
import fr.postgresjson.migration.Migrations
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.ApplicationStopped
|
||||
import io.ktor.application.call
|
||||
import io.ktor.application.install
|
||||
import io.ktor.auth.Authentication
|
||||
import io.ktor.client.HttpClient
|
||||
@@ -51,17 +47,14 @@ import io.ktor.features.CORS
|
||||
import io.ktor.features.CallLogging
|
||||
import io.ktor.features.ContentNegotiation
|
||||
import io.ktor.features.DataConversion
|
||||
import io.ktor.features.NotFoundException
|
||||
import io.ktor.features.StatusPages
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.cio.websocket.pingPeriod
|
||||
import io.ktor.http.cio.websocket.timeout
|
||||
import io.ktor.jackson.jackson
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Locations
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Routing
|
||||
import io.ktor.server.jetty.EngineMain
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
@@ -73,7 +66,6 @@ import org.koin.ktor.ext.Koin
|
||||
import org.koin.ktor.ext.get
|
||||
import org.slf4j.event.Level
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CompletionException
|
||||
|
||||
fun main(args: Array<String>): Unit = EngineMain.main(args)
|
||||
|
||||
@@ -132,7 +124,7 @@ fun Application.module(env: Env = PROD) {
|
||||
}
|
||||
}
|
||||
|
||||
install(Authentication, jwtInstallation(get()))
|
||||
install(Authentication, jwtInstallation(get(), get()))
|
||||
|
||||
install(AutoHeadResponse)
|
||||
|
||||
@@ -171,26 +163,7 @@ fun Application.module(env: Env = PROD) {
|
||||
installDocRoutes()
|
||||
}
|
||||
|
||||
install(StatusPages) {
|
||||
exception<CompletionException> { e ->
|
||||
val parent = e.cause?.cause
|
||||
if (parent is GenericDatabaseException) {
|
||||
call.respond(HttpStatusCode.BadRequest, parent.errorMessage.message!!)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
exception<NotFoundException> { e ->
|
||||
call.respond(HttpStatusCode.NotFound, e.message!!)
|
||||
}
|
||||
exception<AccessDeniedException> {
|
||||
if (call.user == null) call.respond(HttpStatusCode.Unauthorized)
|
||||
else call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
exception<ForbiddenException> {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
}
|
||||
install(StatusPages, statusPagesInstallation())
|
||||
|
||||
install(CORS) {
|
||||
method(HttpMethod.Options)
|
||||
|
||||
@@ -43,4 +43,15 @@ class Configuration(val config: Config) {
|
||||
val rabbitmq: String = config.getString("rabbitmq.connection")
|
||||
val exchangeNotificationName = "notification"
|
||||
val sendGridKey: String = config.getString("mail.sendGrid.key")
|
||||
|
||||
interface Jwt {
|
||||
val secret: String
|
||||
val issuer: String
|
||||
val validityInMs: Int
|
||||
}
|
||||
val jwt = object : Jwt {
|
||||
override val secret = config.getString("jwt.secret")
|
||||
override val issuer = config.getString("jwt.issuer")
|
||||
override val validityInMs = config.getInt("jwt.validity")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package fr.dcproject.application
|
||||
|
||||
import fr.dcproject.application.http.BadRequestException
|
||||
import fr.dcproject.application.http.HttpErrorBadRequest
|
||||
import fr.dcproject.application.http.HttpErrorBadRequest.InvalidParam
|
||||
import io.ktor.features.DataConversion
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
@@ -8,6 +12,7 @@ import org.koin.core.qualifier.Qualifier
|
||||
import java.util.UUID
|
||||
|
||||
private typealias ConverterDeclaration = DataConversion.Configuration.() -> Unit
|
||||
|
||||
private inline fun <reified T> DataConversion.Configuration.get(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
@@ -17,7 +22,21 @@ private inline fun <reified T> DataConversion.Configuration.get(
|
||||
val converters: ConverterDeclaration = {
|
||||
convert<UUID> {
|
||||
decode { values, _ ->
|
||||
try {
|
||||
values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
} catch (e: Throwable) {
|
||||
throw BadRequestException(
|
||||
HttpErrorBadRequest(
|
||||
HttpStatusCode.BadRequest,
|
||||
invalidParams = listOf(
|
||||
InvalidParam(
|
||||
"ID",
|
||||
"must be UUID"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
encode { value ->
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.datatype.joda.JodaModule
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.rabbitmq.client.ConnectionFactory
|
||||
import fr.dcproject.common.email.Mailer
|
||||
import fr.dcproject.component.auth.jwt.JwtConfig
|
||||
import fr.dcproject.component.notification.NotificationConsumer
|
||||
import fr.dcproject.component.notification.NotificationEmailSender
|
||||
import fr.dcproject.component.notification.NotificationsPush
|
||||
@@ -25,6 +26,19 @@ import org.koin.dsl.module
|
||||
|
||||
@KtorExperimentalAPI
|
||||
val KoinModule = module {
|
||||
// JWT
|
||||
single {
|
||||
val config: Configuration = get()
|
||||
JwtConfig(
|
||||
config.jwt.secret,
|
||||
config.jwt.issuer,
|
||||
config.jwt.validityInMs,
|
||||
)
|
||||
}
|
||||
// JWT Verifier
|
||||
single {
|
||||
get<JwtConfig>().verifier
|
||||
}
|
||||
// SQL connection
|
||||
single {
|
||||
val config: Configuration = get()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package fr.dcproject.application.http
|
||||
|
||||
import fr.dcproject.application.http.HttpErrorBadRequest.InvalidParam
|
||||
import io.konform.validation.ValidationResult
|
||||
import io.ktor.http.HttpStatusCode
|
||||
|
||||
class BadRequestException(val httpError: HttpErrorBadRequest) : Exception()
|
||||
|
||||
class HttpErrorBadRequest(
|
||||
statusCode: HttpStatusCode,
|
||||
val title: String = statusCode.description,
|
||||
val invalidParams: List<InvalidParam>,
|
||||
) {
|
||||
val statusCode: Int = statusCode.value
|
||||
data class InvalidParam(
|
||||
val name: String,
|
||||
val reason: String
|
||||
)
|
||||
}
|
||||
|
||||
fun ValidationResult<*>.toOutput() = HttpErrorBadRequest(
|
||||
HttpStatusCode.BadRequest,
|
||||
invalidParams = this.errors.map {
|
||||
InvalidParam(
|
||||
it.dataPath,
|
||||
it.message
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
fun ValidationResult<*>.badRequestIfNotValid() {
|
||||
if (errors.size > 0) {
|
||||
throw BadRequestException(toOutput())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package fr.dcproject.application.http
|
||||
|
||||
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
|
||||
import fr.dcproject.common.security.AccessDeniedException
|
||||
import fr.dcproject.component.auth.ForbiddenException
|
||||
import fr.dcproject.component.auth.user
|
||||
import io.ktor.application.call
|
||||
import io.ktor.features.NotFoundException
|
||||
import io.ktor.features.ParameterConversionException
|
||||
import io.ktor.features.StatusPages
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.response.respond
|
||||
import java.util.concurrent.CompletionException
|
||||
|
||||
class HttpError(
|
||||
statusCode: HttpStatusCode,
|
||||
cause: Throwable? = null,
|
||||
val title: String = cause?.message ?: statusCode.description,
|
||||
) {
|
||||
val statusCode: Int = statusCode.value
|
||||
}
|
||||
|
||||
fun statusPagesInstallation(): StatusPages.Configuration.() -> Unit = {
|
||||
exception<CompletionException> { e ->
|
||||
val parent = e.cause?.cause
|
||||
if (parent is GenericDatabaseException) {
|
||||
HttpError(
|
||||
HttpStatusCode.BadRequest,
|
||||
cause = parent
|
||||
).let {
|
||||
call.respond(HttpStatusCode.BadRequest, it)
|
||||
}
|
||||
} else {
|
||||
HttpError(
|
||||
HttpStatusCode.BadRequest,
|
||||
cause = e
|
||||
).let {
|
||||
call.respond(HttpStatusCode.InternalServerError, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
exception<NotFoundException> { e ->
|
||||
HttpError(
|
||||
HttpStatusCode.NotFound,
|
||||
cause = e
|
||||
).let {
|
||||
call.respond(HttpStatusCode.NotFound, it)
|
||||
}
|
||||
}
|
||||
exception<AccessDeniedException> { e ->
|
||||
if (call.user == null) {
|
||||
HttpError(
|
||||
HttpStatusCode.Unauthorized,
|
||||
cause = e
|
||||
).let {
|
||||
call.respond(HttpStatusCode.Unauthorized, it)
|
||||
}
|
||||
} else {
|
||||
HttpError(
|
||||
HttpStatusCode.Forbidden,
|
||||
cause = e
|
||||
).let {
|
||||
call.respond(HttpStatusCode.Forbidden, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
exception<ForbiddenException> { e ->
|
||||
HttpError(
|
||||
HttpStatusCode.Forbidden,
|
||||
cause = e
|
||||
).let {
|
||||
call.respond(HttpStatusCode.Forbidden, it)
|
||||
}
|
||||
}
|
||||
exception<BadRequestException> { e ->
|
||||
call.respond(HttpStatusCode.BadRequest, e.httpError)
|
||||
}
|
||||
exception<ParameterConversionException> { e ->
|
||||
val parent = e.cause
|
||||
if (parent is BadRequestException) {
|
||||
call.respond(HttpStatusCode.BadRequest, parent.httpError)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,6 @@ interface PaginatedRequestI {
|
||||
}
|
||||
|
||||
open class PaginatedRequest(
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
) : PaginatedRequestI {
|
||||
override val page: Int = if (page < 1) 1 else page
|
||||
override val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
|
||||
}
|
||||
override val page: Int = 1,
|
||||
override val limit: Int = 50
|
||||
) : PaginatedRequestI
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.jayway.jsonpath.JsonPath
|
||||
import com.jayway.jsonpath.PathNotFoundException
|
||||
import org.apache.http.util.EntityUtils
|
||||
import org.elasticsearch.client.Response
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
fun Response.contentToString(): String {
|
||||
return EntityUtils.toString(this.entity)
|
||||
@@ -22,8 +21,6 @@ fun String.getJsonField(jsonPath: String): Int? {
|
||||
return try {
|
||||
JsonPath.read(this, jsonPath)
|
||||
} catch (e: PathNotFoundException) {
|
||||
LoggerFactory.getLogger("fr.dcproject.utils.getJsonField")
|
||||
.warn("No value for Json path ${JsonPath.compile(jsonPath).path}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
4
src/main/kotlin/fr/dcproject/common/utils/Numeric.kt
Normal file
4
src/main/kotlin/fr/dcproject/common/utils/Numeric.kt
Normal file
@@ -0,0 +1,4 @@
|
||||
package fr.dcproject.common.utils
|
||||
|
||||
fun String.isInt(): Boolean = this.toIntOrNull() != null
|
||||
fun String.isBool(): Boolean = this == "true" || this == "false"
|
||||
6
src/main/kotlin/fr/dcproject/common/validation/Email.kt
Normal file
6
src/main/kotlin/fr/dcproject/common/validation/Email.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package fr.dcproject.common.validation
|
||||
|
||||
import io.konform.validation.ValidationBuilder
|
||||
import io.konform.validation.jsonschema.pattern
|
||||
|
||||
fun ValidationBuilder<String>.email() = pattern(""".+@.+\..+""")
|
||||
22
src/main/kotlin/fr/dcproject/common/validation/Password.kt
Normal file
22
src/main/kotlin/fr/dcproject/common/validation/Password.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package fr.dcproject.common.validation
|
||||
|
||||
import io.konform.validation.ValidationBuilder
|
||||
|
||||
fun ValidationBuilder<String>.passwordScore(minScore: Int) =
|
||||
addConstraint("is not enough strong. Use Upper case, Lower case and special characters or juste use more characters.") { value ->
|
||||
value.passwordScore() >= minScore
|
||||
}
|
||||
|
||||
fun String.passwordScore(): Int {
|
||||
var score: Int = length
|
||||
val alphaNum = ('a'..'z').toList() + ('A'..'Z').toList() + ('0'..'9').toList()
|
||||
val specialCount = length - toList().intersect(alphaNum).size
|
||||
score += specialCount.let { if (it > 3) 3 else it }
|
||||
|
||||
val hasAlphaLower = toList().intersect(('a'..'z').toList()).size.let { if (it > 2) 2 else it }
|
||||
val hasAlphaUpper = toList().intersect(('A'..'Z').toList()).size.let { if (it > 2) 2 else it }
|
||||
val hasNum = toList().intersect(('0'..'9').toList()).size.let { if (it > 2) 2 else it }
|
||||
score += (hasAlphaLower + hasAlphaUpper + hasNum - 2) * 2
|
||||
|
||||
return score
|
||||
}
|
||||
14
src/main/kotlin/fr/dcproject/common/validation/Uuid.kt
Normal file
14
src/main/kotlin/fr/dcproject/common/validation/Uuid.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package fr.dcproject.common.validation
|
||||
|
||||
import io.konform.validation.ValidationBuilder
|
||||
import java.util.UUID
|
||||
|
||||
fun ValidationBuilder<String>.isUuid() =
|
||||
addConstraint("must be UUID") {
|
||||
try {
|
||||
UUID.fromString(it)
|
||||
true
|
||||
} catch (exception: IllegalArgumentException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package fr.dcproject.component.article
|
||||
package fr.dcproject.component.article.database
|
||||
|
||||
import fr.dcproject.common.entity.VersionableId
|
||||
import fr.dcproject.common.utils.contentToString
|
||||
import fr.dcproject.common.utils.getJsonField
|
||||
import fr.dcproject.common.utils.toIso
|
||||
import fr.dcproject.component.article.database.ArticleI
|
||||
import fr.dcproject.component.citizen.database.CitizenI
|
||||
import fr.dcproject.component.views.ViewManager
|
||||
import fr.dcproject.component.views.ViewRepository
|
||||
import fr.dcproject.component.views.entity.ViewAggregation
|
||||
import org.elasticsearch.client.Request
|
||||
import org.elasticsearch.client.Response
|
||||
import org.elasticsearch.client.RestClient
|
||||
import org.joda.time.DateTime
|
||||
import java.util.UUID
|
||||
@@ -17,11 +15,11 @@ import java.util.UUID
|
||||
/**
|
||||
* Wrapper for manage views with elasticsearch
|
||||
*/
|
||||
class ArticleViewManager <A> (private val restClient: RestClient) : ViewManager<A> where A : VersionableId, A : ArticleI {
|
||||
class ArticleViewRepository <A> (private val restClient: RestClient) : ViewRepository<A> where A : VersionableId, A : ArticleI {
|
||||
/**
|
||||
* Add view on article to elasticsearch
|
||||
*/
|
||||
override fun addView(ip: String, entity: A, citizen: CitizenI?, dateTime: DateTime): Response? {
|
||||
override fun addView(ip: String, entity: A, citizen: CitizenI?, dateTime: DateTime) {
|
||||
val isLogged = (citizen != null).toString()
|
||||
val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!!
|
||||
val request = Request(
|
||||
@@ -45,7 +43,7 @@ class ArticleViewManager <A> (private val restClient: RestClient) : ViewManager<
|
||||
)
|
||||
}
|
||||
|
||||
return restClient.performRequest(request)
|
||||
restClient.performRequest(request)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,42 +1,69 @@
|
||||
package fr.dcproject.component.article.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.toUUID
|
||||
import fr.dcproject.common.validation.isUuid
|
||||
import fr.dcproject.component.article.ArticleAccessControl
|
||||
import fr.dcproject.component.article.database.ArticleForListing
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.article.database.ArticleRepository
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.routes.PaginatedRequest
|
||||
import fr.dcproject.routes.PaginatedRequestI
|
||||
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.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 FindArticleVersions {
|
||||
@Location("/articles/{article}/versions")
|
||||
class ArticleVersionsRequest(
|
||||
article: UUID,
|
||||
val article: String,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
val sort: String? = null,
|
||||
val direction: RepositoryI.Direction? = null,
|
||||
val search: String? = null
|
||||
) {
|
||||
val page: Int = if (page < 1) 1 else page
|
||||
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
|
||||
val article = ArticleRef(article)
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit) {
|
||||
fun validate() = Validation<ArticleVersionsRequest> {
|
||||
ArticleVersionsRequest::page {
|
||||
minimum(1)
|
||||
maximum(100)
|
||||
}
|
||||
ArticleVersionsRequest::limit {
|
||||
minimum(1)
|
||||
maximum(50)
|
||||
}
|
||||
ArticleVersionsRequest::sort ifPresent {
|
||||
enum(
|
||||
"title",
|
||||
"createdAt",
|
||||
"vote",
|
||||
"popularity",
|
||||
)
|
||||
}
|
||||
ArticleVersionsRequest::article {
|
||||
isUuid()
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
|
||||
private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) =
|
||||
findVersionsById(request.page, request.limit, request.article.id)
|
||||
findVersionsById(request.page, request.limit, request.article.toUUID())
|
||||
|
||||
fun Route.findArticleVersions(repo: ArticleRepository, ac: ArticleAccessControl) {
|
||||
get<ArticleVersionsRequest> {
|
||||
it.validate().badRequestIfNotValid()
|
||||
|
||||
repo.findVersions(it)
|
||||
.apply { ac.assert { canView(result, citizenOrNull) } }
|
||||
.run {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package fr.dcproject.component.article.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.validation.isUuid
|
||||
import fr.dcproject.component.article.ArticleAccessControl
|
||||
import fr.dcproject.component.article.database.ArticleForListing
|
||||
import fr.dcproject.component.article.database.ArticleRepository
|
||||
@@ -10,6 +12,10 @@ import fr.dcproject.routes.PaginatedRequest
|
||||
import fr.dcproject.routes.PaginatedRequestI
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
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.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
@@ -28,7 +34,31 @@ object FindArticles {
|
||||
val search: String? = null,
|
||||
val createdBy: String? = null,
|
||||
val workgroup: String? = null
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit)
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit) {
|
||||
fun validate() = Validation<ArticlesRequest> {
|
||||
ArticlesRequest::page {
|
||||
minimum(1)
|
||||
}
|
||||
ArticlesRequest::limit {
|
||||
minimum(1)
|
||||
maximum(50)
|
||||
}
|
||||
ArticlesRequest::sort ifPresent {
|
||||
enum(
|
||||
"title",
|
||||
"createdAt",
|
||||
"vote",
|
||||
"popularity",
|
||||
)
|
||||
}
|
||||
ArticlesRequest::createdBy ifPresent {
|
||||
isUuid()
|
||||
}
|
||||
ArticlesRequest::workgroup ifPresent {
|
||||
isUuid()
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
|
||||
private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<ArticleForListing> {
|
||||
return find(
|
||||
@@ -43,6 +73,8 @@ object FindArticles {
|
||||
|
||||
fun Route.findArticles(repo: ArticleRepository, ac: ArticleAccessControl) {
|
||||
get<ArticlesRequest> {
|
||||
it.validate().badRequestIfNotValid()
|
||||
|
||||
repo.findArticles(it)
|
||||
.apply { ac.assert { canView(result, citizenOrNull) } }
|
||||
.let {
|
||||
|
||||
@@ -2,10 +2,10 @@ package fr.dcproject.component.article.routes
|
||||
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.component.article.ArticleAccessControl
|
||||
import fr.dcproject.component.article.ArticleViewManager
|
||||
import fr.dcproject.component.article.database.ArticleForView
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.article.database.ArticleRepository
|
||||
import fr.dcproject.component.article.database.ArticleViewRepository
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import io.ktor.application.call
|
||||
import io.ktor.features.NotFoundException
|
||||
@@ -24,7 +24,7 @@ object GetOneArticle {
|
||||
val article = ArticleRef(article)
|
||||
}
|
||||
|
||||
fun Route.getOneArticle(viewManager: ArticleViewManager<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
|
||||
fun Route.getOneArticle(viewRepository: ArticleViewRepository<ArticleForView>, ac: ArticleAccessControl, repo: ArticleRepository) {
|
||||
get<ArticleRequest> {
|
||||
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
|
||||
ac.assert { canView(article, citizenOrNull) }
|
||||
@@ -64,7 +64,7 @@ object GetOneArticle {
|
||||
val total: Int = a.votes.total
|
||||
val score: Int = a.votes.score
|
||||
}
|
||||
val views: Any = viewManager.getViewsCount(article).let { v ->
|
||||
val views: Any = viewRepository.getViewsCount(article).let { v ->
|
||||
object {
|
||||
val total = v.total
|
||||
val unique = v.unique
|
||||
@@ -76,7 +76,7 @@ object GetOneArticle {
|
||||
)
|
||||
|
||||
launch {
|
||||
viewManager.addView(call.request.local.remoteHost, article, citizenOrNull)
|
||||
viewRepository.addView(call.request.local.remoteHost, article, citizenOrNull)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.dcproject.component.article.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.article.ArticleAccessControl
|
||||
@@ -8,9 +9,15 @@ import fr.dcproject.component.article.database.ArticleRepository
|
||||
import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.Input
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.notification.ArticleUpdateNotification
|
||||
import fr.dcproject.component.notification.Publisher
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupRef
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.maxItems
|
||||
import io.konform.validation.jsonschema.maxLength
|
||||
import io.konform.validation.jsonschema.minItems
|
||||
import io.konform.validation.jsonschema.minLength
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.call
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
@@ -34,11 +41,31 @@ object UpsertArticle {
|
||||
val draft: Boolean = false,
|
||||
val versionId: UUID,
|
||||
val workgroup: WorkgroupRef? = null,
|
||||
)
|
||||
) {
|
||||
fun validate() = Validation<Input> {
|
||||
Input::title {
|
||||
minLength(5)
|
||||
maxLength(80)
|
||||
}
|
||||
Input::content {
|
||||
minLength(50)
|
||||
maxLength(6000)
|
||||
}
|
||||
Input::description {
|
||||
minLength(50)
|
||||
maxLength(6000)
|
||||
}
|
||||
Input::tags {
|
||||
minItems(0)
|
||||
maxItems(15)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.upsertArticle(repo: ArticleRepository, publisher: Publisher, ac: ArticleAccessControl) {
|
||||
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest<Input>().run {
|
||||
validate().badRequestIfNotValid()
|
||||
ArticleForUpdate(
|
||||
id = id ?: UUID.randomUUID(),
|
||||
title = title,
|
||||
@@ -54,6 +81,7 @@ object UpsertArticle {
|
||||
}
|
||||
|
||||
post<UpsertArticleRequest> {
|
||||
mustBeAuth()
|
||||
val article = call.convertRequestToEntity()
|
||||
ac.assert { canUpsert(article, citizenOrNull) }
|
||||
repo.upsert(article)?.let { a ->
|
||||
|
||||
@@ -26,7 +26,21 @@ val ApplicationCall.citizenOrNull: CitizenEntity?
|
||||
GlobalContext.get().koin.get<CitizenRepository>().findByUser(it)
|
||||
}
|
||||
|
||||
val ApplicationCall.isAuth: Boolean
|
||||
get() = citizenOrNull == null
|
||||
|
||||
fun ApplicationCall.mustBeAuth() {
|
||||
citizenOrNull ?: throw ForbiddenException("No User Connected")
|
||||
}
|
||||
|
||||
val PipelineContext<Unit, ApplicationCall>.citizen get() = context.citizen
|
||||
val PipelineContext<Unit, ApplicationCall>.citizenOrNull get() = context.citizenOrNull
|
||||
|
||||
val ApplicationCall.user get() = authentication.principal<User>()
|
||||
|
||||
val PipelineContext<Unit, ApplicationCall>.isAuth: Boolean
|
||||
get() = citizenOrNull == null
|
||||
|
||||
fun PipelineContext<Unit, ApplicationCall>.mustBeAuth() {
|
||||
citizenOrNull ?: throw ForbiddenException("No User Connected")
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package fr.dcproject.component.auth.jwt
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import fr.dcproject.component.auth.database.UserI
|
||||
import org.koin.core.context.GlobalContext
|
||||
|
||||
/**
|
||||
* Produce a token for this combination of User and Account
|
||||
*/
|
||||
fun UserI.makeToken(): String = JWT.create()
|
||||
fun UserI.makeToken(): String = GlobalContext.get().koin.get<JwtConfig>().run {
|
||||
JWT.create()
|
||||
.withSubject("Authentication")
|
||||
.withIssuer(JwtConfig.issuer)
|
||||
.withIssuer(issuer)
|
||||
.withClaim("id", id.toString())
|
||||
.withExpiresAt(JwtConfig.getExpiration())
|
||||
.sign(JwtConfig.algorithm)
|
||||
.withExpiresAt(getExpiration())
|
||||
.sign(algorithm)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import com.auth0.jwt.JWTVerifier
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import java.util.Date
|
||||
|
||||
object JwtConfig {
|
||||
private const val secret = "zAP5MBA4B4Ijz0MZaS48"
|
||||
const val issuer = "dc-project.fr"
|
||||
private const val validityInMs = 3_600_000 * 10 // 10 hours
|
||||
|
||||
class JwtConfig(
|
||||
private val secret: String,
|
||||
val issuer: String,
|
||||
private val validityInMs: Int,
|
||||
) {
|
||||
// TODO change to RSA512
|
||||
val algorithm: Algorithm = Algorithm.HMAC512(secret)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.dcproject.component.auth.jwt
|
||||
|
||||
import com.auth0.jwt.JWTVerifier
|
||||
import fr.dcproject.component.auth.database.User
|
||||
import fr.dcproject.component.auth.database.UserRepository
|
||||
import io.ktor.application.ApplicationCall
|
||||
@@ -9,14 +10,14 @@ import io.ktor.http.auth.HttpAuthHeader
|
||||
import io.ktor.routing.Routing
|
||||
import java.util.UUID
|
||||
|
||||
fun jwtInstallation(userRepo: UserRepository): Authentication.Configuration.() -> Unit = {
|
||||
fun jwtInstallation(userRepo: UserRepository, verifier: JWTVerifier): Authentication.Configuration.() -> Unit = {
|
||||
/**
|
||||
* Setup the JWT authentication to be used in [Routing].
|
||||
* If the token is valid, the corresponding [User] is fetched from the database.
|
||||
* The [User] can then be accessed in each [ApplicationCall].
|
||||
*/
|
||||
jwt {
|
||||
verifier(JwtConfig.verifier)
|
||||
verifier(verifier)
|
||||
realm = "dc-project.fr"
|
||||
validate {
|
||||
it.payload.getClaim("id").asString()?.let { id ->
|
||||
@@ -27,7 +28,7 @@ fun jwtInstallation(userRepo: UserRepository): Authentication.Configuration.() -
|
||||
|
||||
/* Token in URL */
|
||||
jwt("url") {
|
||||
verifier(JwtConfig.verifier)
|
||||
verifier(verifier)
|
||||
realm = "dc-project.fr"
|
||||
authHeader { call ->
|
||||
call.request.queryParameters["token"]?.let {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package fr.dcproject.component.auth.routes
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.common.validation.email
|
||||
import fr.dcproject.common.validation.passwordScore
|
||||
import fr.dcproject.component.auth.database.UserForCreate
|
||||
import fr.dcproject.component.auth.database.UserI
|
||||
import fr.dcproject.component.auth.jwt.makeToken
|
||||
@@ -9,6 +12,9 @@ import fr.dcproject.component.auth.routes.Register.RegisterRequest.Input
|
||||
import fr.dcproject.component.citizen.database.CitizenForCreate
|
||||
import fr.dcproject.component.citizen.database.CitizenI
|
||||
import fr.dcproject.component.citizen.database.CitizenRepository
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.maxLength
|
||||
import io.konform.validation.jsonschema.minLength
|
||||
import io.ktor.application.call
|
||||
import io.ktor.features.BadRequestException
|
||||
import io.ktor.http.ContentType
|
||||
@@ -43,6 +49,35 @@ object Register {
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
fun validate() = Validation<Input> {
|
||||
Input::name {
|
||||
Name::firstName {
|
||||
minLength(2)
|
||||
maxLength(50)
|
||||
}
|
||||
Name::lastName {
|
||||
minLength(2)
|
||||
maxLength(50)
|
||||
}
|
||||
Name::civility ifPresent {
|
||||
minLength(1)
|
||||
maxLength(10)
|
||||
}
|
||||
}
|
||||
Input::user {
|
||||
User::username {
|
||||
minLength(7)
|
||||
maxLength(30)
|
||||
}
|
||||
User::password {
|
||||
passwordScore(15)
|
||||
}
|
||||
}
|
||||
Input::email {
|
||||
email()
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +97,10 @@ object Register {
|
||||
|
||||
post<RegisterRequest> {
|
||||
try {
|
||||
val citizen = call.receiveOrBadRequest<Input>().toCitizen()
|
||||
val citizen = call.receiveOrBadRequest<Input>()
|
||||
.apply { validate().badRequestIfNotValid() }
|
||||
.toCitizen()
|
||||
|
||||
citizenRepo.insertWithUser(citizen)?.user?.makeToken()?.let { token ->
|
||||
if (call.request.accept() == ContentType.Application.Json.toString()) {
|
||||
call.respond(
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package fr.dcproject.component.citizen.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.common.validation.passwordScore
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.database.UserRepository
|
||||
import fr.dcproject.component.auth.database.UserWithPassword
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.citizen.CitizenAccessControl
|
||||
import fr.dcproject.component.citizen.database.CitizenRef
|
||||
import io.konform.validation.Validation
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.UserPasswordCredential
|
||||
import io.ktor.features.BadRequestException
|
||||
@@ -24,13 +28,21 @@ object ChangeMyPassword {
|
||||
@Location("/citizens/{citizen}/password/change")
|
||||
class ChangePasswordCitizenRequest(citizen: UUID) {
|
||||
val citizen = CitizenRef(citizen)
|
||||
data class Input(val oldPassword: String, val newPassword: String)
|
||||
data class Input(val oldPassword: String, val newPassword: String) {
|
||||
fun validate() = Validation<Input> {
|
||||
Input::newPassword {
|
||||
passwordScore(15)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) {
|
||||
put<ChangePasswordCitizenRequest> {
|
||||
ac.assert { canChangePassword(it.citizen, citizenOrNull) }
|
||||
mustBeAuth()
|
||||
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
|
||||
.apply { validate().badRequestIfNotValid() }
|
||||
ac.assert { canChangePassword(it.citizen, citizenOrNull) }
|
||||
userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password")
|
||||
userRepository.changePassword(
|
||||
UserWithPassword(
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package fr.dcproject.component.citizen.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
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.CitizenAccessControl
|
||||
import fr.dcproject.component.citizen.database.CitizenCreator
|
||||
import fr.dcproject.component.citizen.database.CitizenRepository
|
||||
import fr.dcproject.routes.PaginatedRequest
|
||||
import fr.dcproject.routes.PaginatedRequestI
|
||||
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.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
@@ -26,10 +32,28 @@ object FindCitizens {
|
||||
val sort: String? = null,
|
||||
val direction: RepositoryI.Direction? = null,
|
||||
val search: String? = null
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit)
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit) {
|
||||
fun validate() = Validation<CitizensRequest> {
|
||||
CitizensRequest::page {
|
||||
minimum(1)
|
||||
}
|
||||
CitizensRequest::limit {
|
||||
minimum(1)
|
||||
maximum(50)
|
||||
}
|
||||
CitizensRequest::sort ifPresent {
|
||||
enum(
|
||||
"title",
|
||||
"createdAt",
|
||||
)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
|
||||
fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) {
|
||||
get<CitizensRequest> {
|
||||
mustBeAuth()
|
||||
it.validate().badRequestIfNotValid()
|
||||
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
|
||||
ac.assert { canView(citizens.result, citizenOrNull) }
|
||||
call.respond(
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes
|
||||
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.CitizenAccessControl
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
@@ -22,6 +23,7 @@ object GetCurrentCitizen {
|
||||
|
||||
fun Route.getCurrentCitizen(ac: CitizenAccessControl) {
|
||||
get<CurrentCitizenRequest> {
|
||||
mustBeAuth()
|
||||
val currentUser = citizenOrNull
|
||||
if (currentUser === null) {
|
||||
call.respond(HttpStatusCode.Unauthorized)
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes
|
||||
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.CitizenAccessControl
|
||||
import fr.dcproject.component.citizen.database.CitizenRef
|
||||
import fr.dcproject.component.citizen.database.CitizenRepository
|
||||
@@ -26,6 +27,7 @@ object GetOneCitizen {
|
||||
|
||||
fun Route.getOneCitizen(ac: CitizenAccessControl, citizenRepository: CitizenRepository) {
|
||||
get<CitizenRequest> {
|
||||
mustBeAuth()
|
||||
val citizen = citizenRepository.findById(it.citizen.id) ?: throw NotFoundException("Citizen not found ${it.citizen.id}")
|
||||
ac.assert { canView(citizen, citizenOrNull) }
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
|
||||
target: EntityI,
|
||||
page: Int,
|
||||
limit: Int,
|
||||
sort: Sort
|
||||
sort: String
|
||||
): Paginated<CommentForView<ArticleForView, CitizenCreatorI>> {
|
||||
return requester
|
||||
.getFunction("find_comments_by_target")
|
||||
@@ -49,18 +49,7 @@ class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<Arti
|
||||
page,
|
||||
limit,
|
||||
"target_id" to target.id,
|
||||
"sort" to sort.sql
|
||||
"sort" to sort
|
||||
) as Paginated<CommentForView<ArticleForView, CitizenCreatorI>>
|
||||
}
|
||||
|
||||
enum class Sort(val sql: String) {
|
||||
CREATED_AT("created_at"),
|
||||
VOTES("votes");
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): Sort? {
|
||||
return values().firstOrNull { it.sql == string }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package fr.dcproject.component.comment.article.routes
|
||||
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.comment.article.database.CommentArticleRepository
|
||||
import fr.dcproject.component.comment.article.routes.CreateCommentArticle.PostArticleCommentRequest.Input
|
||||
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
import fr.dcproject.component.comment.generic.database.CommentForUpdate
|
||||
import fr.dcproject.component.comment.toOutput
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.maxLength
|
||||
import io.konform.validation.jsonschema.minLength
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
@@ -25,12 +29,22 @@ object CreateCommentArticle {
|
||||
@Location("/articles/{article}/comments")
|
||||
class PostArticleCommentRequest(article: UUID) {
|
||||
val article = ArticleRef(article)
|
||||
class Input(val content: String)
|
||||
class Input(val content: String) {
|
||||
fun validate() = Validation<Input> {
|
||||
Input::content {
|
||||
minLength(20)
|
||||
maxLength(6000)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) {
|
||||
post<PostArticleCommentRequest> {
|
||||
call.receiveOrBadRequest<Input>().run {
|
||||
mustBeAuth()
|
||||
call.receiveOrBadRequest<Input>()
|
||||
.apply { validate().badRequestIfNotValid() }
|
||||
.run {
|
||||
CommentForUpdate(
|
||||
target = it.article,
|
||||
createdBy = citizen,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.dcproject.component.comment.article.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
@@ -9,6 +10,10 @@ import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
import fr.dcproject.component.comment.toOutput
|
||||
import fr.dcproject.routes.PaginatedRequest
|
||||
import fr.dcproject.routes.PaginatedRequestI
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.enum
|
||||
import io.konform.validation.jsonschema.maximum
|
||||
import io.konform.validation.jsonschema.minimum
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
@@ -26,14 +31,31 @@ object GetArticleComments {
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
val search: String? = null,
|
||||
sort: String = CommentArticleRepository.Sort.CREATED_AT.sql
|
||||
val sort: String = "createdAt"
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit) {
|
||||
val article = ArticleRef(article)
|
||||
val sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.fromString(sort) ?: CommentArticleRepository.Sort.CREATED_AT
|
||||
|
||||
fun validate() = Validation<ArticleCommentsRequest> {
|
||||
ArticleCommentsRequest::page {
|
||||
minimum(1)
|
||||
}
|
||||
ArticleCommentsRequest::limit {
|
||||
minimum(1)
|
||||
maximum(50)
|
||||
}
|
||||
ArticleCommentsRequest::sort ifPresent {
|
||||
enum(
|
||||
"votes",
|
||||
"createdAt",
|
||||
)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
|
||||
fun Route.getArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) {
|
||||
get<ArticleCommentsRequest> {
|
||||
it.validate().badRequestIfNotValid()
|
||||
|
||||
val comments = repo.findByTarget(it.article, it.page, it.limit, it.sort)
|
||||
if (comments.result.isNotEmpty()) {
|
||||
ac.assert { canView(comments.result, citizenOrNull) }
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.comment.article.routes
|
||||
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.comment.article.database.CommentArticleRepository
|
||||
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
@@ -25,6 +26,7 @@ object GetCitizenArticleComments {
|
||||
|
||||
fun Route.getCitizenArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) {
|
||||
get<CitizenCommentArticleRequest> {
|
||||
mustBeAuth()
|
||||
repo.findByCitizen(it.citizen).let { comments ->
|
||||
ac.assert { canView(comments.result, citizenOrNull) }
|
||||
call.respond(
|
||||
|
||||
@@ -5,7 +5,6 @@ import fr.dcproject.common.entity.TargetI
|
||||
import fr.dcproject.component.citizen.database.CitizenCreator
|
||||
import fr.dcproject.component.citizen.database.CitizenCreatorI
|
||||
import fr.dcproject.component.citizen.database.CitizenI
|
||||
import fr.dcproject.component.comment.article.database.CommentArticleRepository
|
||||
import fr.dcproject.component.comment.generic.database.CommentForView
|
||||
import fr.dcproject.component.comment.generic.database.CommentRepositoryAbs
|
||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||
@@ -41,7 +40,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
|
||||
target: EntityI,
|
||||
page: Int,
|
||||
limit: Int,
|
||||
sort: CommentArticleRepository.Sort
|
||||
sort: String
|
||||
): Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_target")
|
||||
@@ -49,7 +48,7 @@ class CommentConstitutionRepository(requester: Requester) : CommentRepositoryAbs
|
||||
page,
|
||||
limit,
|
||||
"target_id" to target.id,
|
||||
"sort" to sort.sql
|
||||
"sort" to sort
|
||||
)
|
||||
as Paginated<CommentForView<ConstitutionRef, CitizenCreatorI>>
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package fr.dcproject.component.comment.constitution.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.comment.constitution.database.CommentConstitutionRepository
|
||||
import fr.dcproject.component.comment.constitution.routes.CreateConstitutionComment.CreateConstitutionCommentRequest.Input
|
||||
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
import fr.dcproject.component.comment.generic.database.CommentForUpdate
|
||||
import fr.dcproject.component.comment.toOutput
|
||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.maxLength
|
||||
import io.konform.validation.jsonschema.minLength
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
@@ -25,12 +30,23 @@ object CreateConstitutionComment {
|
||||
@Location("/constitutions/{constitution}/comments")
|
||||
class CreateConstitutionCommentRequest(constitution: UUID) {
|
||||
val constitution = ConstitutionRef(constitution)
|
||||
class Input(val content: String)
|
||||
class Input(val content: String) {
|
||||
fun validate() = Validation<Input> {
|
||||
Input::content {
|
||||
minLength(20)
|
||||
maxLength(6000)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
|
||||
post<CreateConstitutionCommentRequest> {
|
||||
call.receiveOrBadRequest<Input>().run {
|
||||
mustBeAuth()
|
||||
|
||||
call.receiveOrBadRequest<Input>()
|
||||
.apply { validate().badRequestIfNotValid() }
|
||||
.run {
|
||||
CommentForUpdate(
|
||||
target = it.constitution,
|
||||
createdBy = citizen,
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.comment.constitution.routes
|
||||
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.comment.constitution.database.CommentConstitutionRepository
|
||||
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
@@ -25,6 +26,7 @@ object GetCitizenCommentConstitution {
|
||||
|
||||
fun Route.getCitizenCommentConstitution(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
|
||||
get<GetCitizenCommentConstitutionRequest> {
|
||||
mustBeAuth()
|
||||
val comments = repo.findByCitizen(it.citizen)
|
||||
ac.assert { canView(comments.result, citizenOrNull) }
|
||||
call.respond(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.dcproject.component.comment.constitution.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
@@ -7,6 +8,12 @@ import fr.dcproject.component.comment.constitution.database.CommentConstitutionR
|
||||
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
import fr.dcproject.component.comment.toOutput
|
||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||
import fr.dcproject.routes.PaginatedRequest
|
||||
import fr.dcproject.routes.PaginatedRequestI
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.enum
|
||||
import io.konform.validation.jsonschema.maximum
|
||||
import io.konform.validation.jsonschema.minimum
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
@@ -19,12 +26,36 @@ import java.util.UUID
|
||||
@KtorExperimentalLocationsAPI
|
||||
object GetConstitutionComment {
|
||||
@Location("/constitutions/{constitution}/comments")
|
||||
class GetConstitutionCommentRequest(constitution: UUID) {
|
||||
class GetConstitutionCommentRequest(
|
||||
constitution: UUID,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
val search: String? = null,
|
||||
val sort: String = "createdAt"
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit) {
|
||||
val constitution = ConstitutionRef(constitution)
|
||||
|
||||
fun validate() = Validation<GetConstitutionCommentRequest> {
|
||||
GetConstitutionCommentRequest::page {
|
||||
minimum(1)
|
||||
}
|
||||
GetConstitutionCommentRequest::limit {
|
||||
minimum(1)
|
||||
maximum(50)
|
||||
}
|
||||
GetConstitutionCommentRequest::sort ifPresent {
|
||||
enum(
|
||||
"votes",
|
||||
"createdAt",
|
||||
)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
|
||||
fun Route.getConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
|
||||
get<GetConstitutionCommentRequest> {
|
||||
it.validate().badRequestIfNotValid()
|
||||
|
||||
val comments = repo.findByTarget(it.constitution)
|
||||
ac.assert { canView(comments.result, citizenOrNull) }
|
||||
call.respond(
|
||||
|
||||
@@ -63,12 +63,14 @@ open class CommentForUpdate<T : TargetI, C : CitizenI>(
|
||||
constructor(
|
||||
createdBy: C,
|
||||
parent: CommentParent<T>,
|
||||
content: String
|
||||
content: String,
|
||||
id: UUID? = null,
|
||||
) : this(
|
||||
createdBy = createdBy,
|
||||
parent = parent,
|
||||
target = parent.target,
|
||||
content = content
|
||||
content = content,
|
||||
id = id ?: UUID.randomUUID(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import fr.dcproject.common.entity.TargetRef
|
||||
import fr.dcproject.component.citizen.database.CitizenCreator
|
||||
import fr.dcproject.component.citizen.database.CitizenCreatorI
|
||||
import fr.dcproject.component.citizen.database.CitizenI
|
||||
import fr.dcproject.component.comment.article.database.CommentArticleRepository
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
@@ -49,7 +48,7 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
|
||||
target: EntityI,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
|
||||
sort: String = "createdAt"
|
||||
): Paginated<CommentForView<T, CitizenCreatorI>> {
|
||||
return findByTarget(target.id, page, limit, sort)
|
||||
}
|
||||
@@ -58,36 +57,30 @@ abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Request
|
||||
targetId: UUID,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
|
||||
): Paginated<CommentForView<T, CitizenCreatorI>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_target")
|
||||
sort: String = "createdAt"
|
||||
): Paginated<CommentForView<T, CitizenCreatorI>> = requester
|
||||
.getFunction("find_comments_by_target")
|
||||
.select<CommentForView<T, CitizenCreator>>(
|
||||
page,
|
||||
limit,
|
||||
"target_id" to targetId,
|
||||
"sort" to sort.sql
|
||||
)
|
||||
as Paginated<CommentForView<T, CitizenCreatorI>>
|
||||
}
|
||||
}
|
||||
"sort" to sort
|
||||
) as Paginated<CommentForView<T, CitizenCreatorI>>
|
||||
|
||||
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>) {
|
||||
requester
|
||||
fun <I : TargetI, C : CitizenCreatorI> comment(comment: CommentForUpdate<I, C>): CommentForView<TargetRef, CitizenCreator> = requester
|
||||
.getFunction("comment")
|
||||
.sendQuery(
|
||||
.selectOne(
|
||||
"reference" to comment.target.reference,
|
||||
"resource" to comment
|
||||
)
|
||||
}
|
||||
)!!
|
||||
|
||||
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>) {
|
||||
requester
|
||||
fun <I : T> edit(comment: CommentForUpdate<I, CitizenCreatorI>): CommentForView<TargetRef, CitizenCreator> {
|
||||
return requester
|
||||
.getFunction("edit_comment")
|
||||
.sendQuery(
|
||||
.selectOne(
|
||||
"id" to comment.id,
|
||||
"content" to comment.content
|
||||
)
|
||||
)!!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package fr.dcproject.component.comment.generic.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
import fr.dcproject.component.comment.generic.database.CommentForUpdate
|
||||
import fr.dcproject.component.comment.generic.database.CommentRef
|
||||
import fr.dcproject.component.comment.generic.database.CommentRepository
|
||||
import fr.dcproject.component.comment.toOutput
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.maxLength
|
||||
import io.konform.validation.jsonschema.minLength
|
||||
import io.ktor.application.call
|
||||
import io.ktor.features.NotFoundException
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
import io.ktor.locations.post
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import java.util.UUID
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object CreateComment {
|
||||
@Location("/comments/{comment}")
|
||||
class CreateCommentRequest(comment: UUID) {
|
||||
val comment = CommentRef(comment)
|
||||
class Input(val content: String) {
|
||||
fun validate() = Validation<Input> {
|
||||
Input::content {
|
||||
minLength(20)
|
||||
maxLength(6000)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
|
||||
post<CreateCommentRequest> {
|
||||
mustBeAuth()
|
||||
|
||||
call.receiveOrBadRequest<CreateCommentRequest.Input>()
|
||||
.apply { validate().badRequestIfNotValid() }
|
||||
.run {
|
||||
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
||||
CommentForUpdate(
|
||||
content = content,
|
||||
createdBy = citizen,
|
||||
target = parent.target,
|
||||
parent = parent,
|
||||
)
|
||||
}.let { newComment ->
|
||||
ac.assert { canCreate(newComment, citizenOrNull) }
|
||||
repo.comment(newComment)
|
||||
call.respond(HttpStatusCode.Created, newComment.toOutput())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package fr.dcproject.component.comment.generic.routes
|
||||
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
import fr.dcproject.component.comment.generic.database.CommentForUpdate
|
||||
import fr.dcproject.component.comment.generic.database.CommentRef
|
||||
import fr.dcproject.component.comment.generic.database.CommentRepository
|
||||
import fr.dcproject.component.comment.toOutput
|
||||
import io.ktor.application.call
|
||||
import io.ktor.features.NotFoundException
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
import io.ktor.locations.post
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import java.util.UUID
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object CreateCommentChildren {
|
||||
@Location("/comments/{comment}/children")
|
||||
class CreateCommentChildrenRequest(comment: UUID) {
|
||||
val comment = CommentRef(comment)
|
||||
class Input(val content: String)
|
||||
}
|
||||
|
||||
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
|
||||
post<CreateCommentChildrenRequest> {
|
||||
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
||||
val newComment = CommentForUpdate(
|
||||
content = call.receiveOrBadRequest<CreateCommentChildrenRequest.Input>().content,
|
||||
createdBy = citizen,
|
||||
parent = parent
|
||||
)
|
||||
|
||||
ac.assert { canCreate(newComment, citizenOrNull) }
|
||||
repo.comment(newComment)
|
||||
|
||||
call.respond(HttpStatusCode.Created, newComment.toOutput())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package fr.dcproject.component.comment.generic.routes
|
||||
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.comment.generic.CommentAccessControl
|
||||
import fr.dcproject.component.comment.generic.database.CommentForUpdate
|
||||
import fr.dcproject.component.comment.generic.database.CommentRef
|
||||
import fr.dcproject.component.comment.generic.database.CommentRepository
|
||||
import fr.dcproject.component.comment.toOutput
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.maxLength
|
||||
import io.konform.validation.jsonschema.minLength
|
||||
import io.ktor.application.call
|
||||
import io.ktor.features.NotFoundException
|
||||
import io.ktor.http.HttpStatusCode
|
||||
@@ -23,21 +28,40 @@ object EditComment {
|
||||
@Location("/comments/{comment}")
|
||||
class EditCommentRequest(comment: UUID) {
|
||||
val comment = CommentRef(comment)
|
||||
class Input(val content: String)
|
||||
class Input(val content: String) {
|
||||
fun validate() = Validation<Input> {
|
||||
Input::content {
|
||||
minLength(20)
|
||||
maxLength(6000)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) {
|
||||
put<EditCommentRequest> {
|
||||
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
||||
ac.assert { canUpdate(comment, citizenOrNull) }
|
||||
|
||||
comment.content = call.receiveOrBadRequest<EditCommentRequest.Input>().content
|
||||
repo.edit(comment)
|
||||
mustBeAuth()
|
||||
val commentOld = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
||||
ac.assert { canUpdate(commentOld, citizenOrNull) }
|
||||
|
||||
call.receiveOrBadRequest<EditCommentRequest.Input>()
|
||||
.apply { validate().badRequestIfNotValid() }
|
||||
.run {
|
||||
CommentForUpdate(
|
||||
id = commentOld.id,
|
||||
createdBy = commentOld.createdBy,
|
||||
target = commentOld.target,
|
||||
parent = commentOld.parent,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
.let { repo.edit(it) }
|
||||
.let {
|
||||
call.respond(
|
||||
HttpStatusCode.OK,
|
||||
comment.toOutput()
|
||||
it.toOutput()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package fr.dcproject.component.comment.generic.routes
|
||||
|
||||
import fr.dcproject.component.comment.generic.routes.CreateCommentChildren.createCommentChildren
|
||||
import fr.dcproject.component.comment.generic.routes.CreateComment.createCommentChildren
|
||||
import fr.dcproject.component.comment.generic.routes.EditComment.editComment
|
||||
import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments
|
||||
import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package fr.dcproject.component.constitution.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.citizen.database.Citizen
|
||||
import fr.dcproject.component.citizen.database.CitizenWithUserI
|
||||
import fr.dcproject.component.constitution.ConstitutionAccessControl
|
||||
@@ -14,6 +16,9 @@ import fr.dcproject.component.constitution.database.ConstitutionForUpdate.TitleF
|
||||
import fr.dcproject.component.constitution.database.ConstitutionRepository
|
||||
import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input
|
||||
import fr.dcproject.component.constitution.routes.CreateConstitution.PostConstitutionRequest.Input.Title
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.maxLength
|
||||
import io.konform.validation.jsonschema.minLength
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
@@ -35,7 +40,6 @@ object CreateConstitution {
|
||||
val draft: Boolean = false,
|
||||
val versionId: UUID = UUID.randomUUID()
|
||||
) {
|
||||
|
||||
class Title(
|
||||
val id: UUID = UUID.randomUUID(),
|
||||
val name: String,
|
||||
@@ -43,10 +47,25 @@ object CreateConstitution {
|
||||
) {
|
||||
class ArticleRef(val id: UUID)
|
||||
}
|
||||
|
||||
fun validate() = Validation<Input> {
|
||||
Input::title {
|
||||
minLength(10)
|
||||
maxLength(80)
|
||||
}
|
||||
Input::titles onEach {
|
||||
Title::name {
|
||||
minLength(10)
|
||||
maxLength(80)
|
||||
}
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNewConstitution(input: Input, citizen: Citizen) = input.run {
|
||||
validate().badRequestIfNotValid()
|
||||
|
||||
ConstitutionForUpdate<CitizenWithUserI, TitleForUpdate<ArticleRef>>(
|
||||
id = UUID.randomUUID(),
|
||||
title = title,
|
||||
@@ -68,6 +87,7 @@ object CreateConstitution {
|
||||
|
||||
fun Route.createConstitution(repo: ConstitutionRepository, ac: ConstitutionAccessControl) {
|
||||
post<PostConstitutionRequest> {
|
||||
mustBeAuth()
|
||||
getNewConstitution(call.receiveOrBadRequest(), citizen).let {
|
||||
ac.assert { canCreate(it, citizenOrNull) }
|
||||
val c = repo.upsert(it) ?: error("Unable to create Constitution")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.dcproject.component.constitution.routes
|
||||
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
@@ -8,6 +9,10 @@ import fr.dcproject.component.constitution.database.ConstitutionRepository
|
||||
import fr.dcproject.routes.PaginatedRequest
|
||||
import fr.dcproject.routes.PaginatedRequestI
|
||||
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.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
@@ -27,10 +32,27 @@ object FindConstitutions {
|
||||
val sort: String? = null,
|
||||
val direction: RepositoryI.Direction? = null,
|
||||
val search: String? = null
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit)
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit) {
|
||||
fun validate() = Validation<FindConstitutionsRequest> {
|
||||
FindConstitutionsRequest::page {
|
||||
minimum(1)
|
||||
}
|
||||
FindConstitutionsRequest::limit {
|
||||
minimum(1)
|
||||
maximum(50)
|
||||
}
|
||||
FindConstitutionsRequest::sort ifPresent {
|
||||
enum(
|
||||
"title",
|
||||
"createdAt",
|
||||
)
|
||||
}
|
||||
}.validate(this)
|
||||
}
|
||||
|
||||
fun Route.findConstitutions(repo: ConstitutionRepository, ac: ConstitutionAccessControl) {
|
||||
get<FindConstitutionsRequest> {
|
||||
it.validate().badRequestIfNotValid()
|
||||
val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
|
||||
ac.assert { canView(constitutions.result, citizenOrNull) }
|
||||
call.respond(
|
||||
|
||||
@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.follow.FollowAccessControl
|
||||
import fr.dcproject.component.follow.database.FollowArticleRepository
|
||||
import fr.dcproject.component.follow.database.FollowForUpdate
|
||||
@@ -25,6 +26,7 @@ object FollowArticle {
|
||||
|
||||
fun Route.followArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
|
||||
post<ArticleFollowRequest> {
|
||||
mustBeAuth()
|
||||
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
|
||||
ac.assert { canCreate(follow, citizenOrNull) }
|
||||
repo.follow(follow)
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.article
|
||||
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.FollowArticleRepository
|
||||
@@ -25,6 +26,7 @@ object GetMyFollowsArticle {
|
||||
|
||||
fun Route.getMyFollowsArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
|
||||
get<CitizenFollowArticleRequest> {
|
||||
mustBeAuth()
|
||||
val follows = repo.findByCitizen(it.citizen)
|
||||
ac.assert { canView(follows.result, citizenOrNull) }
|
||||
call.respond(
|
||||
|
||||
@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.follow.FollowAccessControl
|
||||
import fr.dcproject.component.follow.database.FollowArticleRepository
|
||||
import fr.dcproject.component.follow.database.FollowForUpdate
|
||||
@@ -25,6 +26,7 @@ object UnfollowArticle {
|
||||
|
||||
fun Route.unfollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
|
||||
delete<ArticleFollowRequest> {
|
||||
mustBeAuth()
|
||||
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
|
||||
ac.assert { canDelete(follow, citizenOrNull) }
|
||||
repo.unfollow(follow)
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
|
||||
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.constitution.database.ConstitutionRef
|
||||
import fr.dcproject.component.follow.FollowAccessControl
|
||||
import fr.dcproject.component.follow.database.FollowConstitutionRepository
|
||||
@@ -25,6 +26,7 @@ object FollowConstitution {
|
||||
|
||||
fun Route.followConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
|
||||
post<ConstitutionFollowRequest> {
|
||||
mustBeAuth()
|
||||
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
|
||||
ac.assert { canCreate(follow, citizenOrNull) }
|
||||
repo.follow(follow)
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
|
||||
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.FollowConstitutionRepository
|
||||
@@ -25,6 +26,7 @@ object GetMyFollowsConstitution {
|
||||
|
||||
fun Route.getMyFollowsConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
|
||||
get<CitizenFollowConstitutionRequest> {
|
||||
mustBeAuth()
|
||||
val follows = repo.findByCitizen(it.citizen)
|
||||
ac.assert { canView(follows.result, citizenOrNull) }
|
||||
call.respond(
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
|
||||
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.constitution.database.ConstitutionRef
|
||||
import fr.dcproject.component.follow.FollowAccessControl
|
||||
import fr.dcproject.component.follow.database.FollowConstitutionRepository
|
||||
@@ -25,6 +26,7 @@ object UnfollowConstitution {
|
||||
|
||||
fun Route.unfollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
|
||||
delete<ConstitutionUnfollowRequest> {
|
||||
mustBeAuth()
|
||||
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
|
||||
ac.assert { canDelete(follow, citizenOrNull) }
|
||||
repo.unfollow(follow)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package fr.dcproject.component.notification
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
@@ -9,9 +11,15 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import fr.dcproject.common.entity.Entity
|
||||
import fr.dcproject.component.article.database.ArticleForView
|
||||
import io.konform.validation.Validation
|
||||
import io.konform.validation.jsonschema.enum
|
||||
import org.joda.time.DateTime
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
|
||||
@JsonSubTypes(
|
||||
JsonSubTypes.Type(value = ArticleUpdateNotification::class, name = "article")
|
||||
)
|
||||
open class Notification(
|
||||
val type: String,
|
||||
val createdAt: DateTime = DateTime.now()
|
||||
@@ -44,6 +52,14 @@ open class Notification(
|
||||
|
||||
inline fun <reified T : Notification> fromString(raw: String): T = mapper.readValue(raw)
|
||||
}
|
||||
|
||||
fun getValidation() = Validation<Notification> {
|
||||
Notification::type {
|
||||
enum(
|
||||
"article"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class EntityNotification(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.dcproject.component.notification
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException
|
||||
import fr.dcproject.application.http.badRequestIfNotValid
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.citizen.database.CitizenI
|
||||
import io.ktor.http.cio.websocket.Frame
|
||||
@@ -28,12 +29,12 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class NotificationsPush private constructor(
|
||||
class NotificationsPush (
|
||||
private val redis: RedisAsyncCommands<String, String>,
|
||||
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
|
||||
citizen: CitizenI,
|
||||
incoming: Flow<Notification>,
|
||||
onRecieve: suspend (Notification) -> Unit,
|
||||
onReceive: suspend (Notification) -> Unit,
|
||||
) {
|
||||
class Builder(val redisClient: RedisClient) {
|
||||
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
|
||||
@@ -43,8 +44,8 @@ class NotificationsPush private constructor(
|
||||
fun build(
|
||||
citizen: CitizenI,
|
||||
incoming: Flow<Notification>,
|
||||
onRecieve: suspend (Notification) -> Unit,
|
||||
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve)
|
||||
onReceive: suspend (Notification) -> Unit,
|
||||
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onReceive)
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
|
||||
@@ -52,7 +53,10 @@ class NotificationsPush private constructor(
|
||||
val incomingFlow: Flow<Notification> = ws.incoming.consumeAsFlow()
|
||||
.mapNotNull<Frame, Text> { it as? Frame.Text }
|
||||
.map { it.readText() }
|
||||
.map { Notification.fromString(it) }
|
||||
.map {
|
||||
Notification.fromString<Notification>(it)
|
||||
.apply { getValidation().validate(this).badRequestIfNotValid() }
|
||||
}
|
||||
|
||||
return build(ws.call.citizen, incomingFlow) {
|
||||
ws.outgoing.send(Text(it.toString()))
|
||||
@@ -69,7 +73,7 @@ class NotificationsPush private constructor(
|
||||
override fun message(pattern: String?, channel: String?, message: String?) {
|
||||
runBlocking {
|
||||
getNotifications().collect {
|
||||
onRecieve(it)
|
||||
onReceive(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,10 +89,12 @@ class NotificationsPush private constructor(
|
||||
|
||||
/* Get old notification and sent it to websocket */
|
||||
runBlocking {
|
||||
getNotifications().collect { onRecieve(it) }
|
||||
getNotifications().collect {
|
||||
onReceive(it)
|
||||
}
|
||||
}
|
||||
|
||||
/* Lisen redis event, and sent the new notification into websocket */
|
||||
/* Listen redis event, and sent the new notification into websocket */
|
||||
redisConnectionPubSub.run {
|
||||
addListener(listener)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.toUUID
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.citizen.database.CitizenRef
|
||||
import fr.dcproject.component.opinion.OpinionAccessControl
|
||||
import fr.dcproject.component.opinion.database.Opinion
|
||||
@@ -31,6 +32,7 @@ object GetCitizenOpinions {
|
||||
|
||||
fun Route.getCitizenOpinions(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
|
||||
get<CitizenOpinions> {
|
||||
mustBeAuth()
|
||||
val opinionsEntities: List<Opinion<ArticleRef>> = repo.findCitizenOpinionsByTargets(it.citizen, it.id)
|
||||
ac.assert { canView(opinionsEntities, citizenOrNull) }
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.auth.mustBeAuth
|
||||
import fr.dcproject.component.citizen.database.CitizenRef
|
||||
import fr.dcproject.component.opinion.OpinionAccessControl
|
||||
import fr.dcproject.component.opinion.database.Opinion
|
||||
@@ -37,6 +38,7 @@ object GetMyOpinionsArticle {
|
||||
|
||||
fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
|
||||
get<CitizenOpinionsArticleRequest> {
|
||||
mustBeAuth()
|
||||
val opinions: Paginated<Opinion<TargetRef>> = repo.findCitizenOpinions(citizen, it.page, it.limit)
|
||||
ac.assert { canView(opinions.result, citizenOrNull) }
|
||||
call.respond(
|
||||
|
||||
@@ -6,6 +6,7 @@ import fr.dcproject.common.utils.toUUID
|
||||
import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.opinion.OpinionAccessControl
|
||||
import fr.dcproject.component.opinion.database.OpinionChoiceRef
|
||||
import fr.dcproject.component.opinion.database.OpinionForUpdate
|
||||
@@ -34,6 +35,7 @@ object OpinionArticle {
|
||||
|
||||
fun Route.setOpinionOnArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
|
||||
put<ArticleOpinion> {
|
||||
mustBeAuth()
|
||||
call.receiveOrBadRequest<ArticleOpinion.Body>().ids.map { id ->
|
||||
OpinionForUpdate(
|
||||
choice = OpinionChoiceRef(id),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package fr.dcproject.component.views
|
||||
|
||||
import fr.dcproject.application.Configuration
|
||||
import fr.dcproject.component.article.ArticleViewManager
|
||||
import fr.dcproject.component.article.database.ArticleForView
|
||||
import fr.dcproject.component.article.database.ArticleViewRepository
|
||||
import org.apache.http.HttpHost
|
||||
import org.elasticsearch.client.RestClient
|
||||
import org.koin.dsl.module
|
||||
@@ -17,6 +17,6 @@ val viewKoinModule = module {
|
||||
).build().apply {
|
||||
createEsIndexForViews()
|
||||
}
|
||||
ArticleViewManager<ArticleForView>(esClient)
|
||||
ArticleViewRepository<ArticleForView>(esClient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ package fr.dcproject.component.views
|
||||
|
||||
import fr.dcproject.component.citizen.database.CitizenI
|
||||
import fr.dcproject.component.views.entity.ViewAggregation
|
||||
import org.elasticsearch.client.Response
|
||||
import org.joda.time.DateTime
|
||||
|
||||
interface ViewManager <T> {
|
||||
interface ViewRepository <T> {
|
||||
/**
|
||||
* Add view to one entity
|
||||
*/
|
||||
fun addView(ip: String, entity: T, citizen: CitizenI? = null, dateTime: DateTime = DateTime.now()): Response?
|
||||
fun addView(ip: String, entity: T, citizen: CitizenI? = null, dateTime: DateTime = DateTime.now())
|
||||
|
||||
/**
|
||||
* Get Views aggregations
|
||||
@@ -4,6 +4,7 @@ import fr.dcproject.common.response.toOutput
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.toUUID
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.citizen.database.CitizenRef
|
||||
import fr.dcproject.component.vote.VoteAccessControl
|
||||
import fr.dcproject.component.vote.database.VoteRepository
|
||||
@@ -26,6 +27,7 @@ object GetCitizenVotes {
|
||||
|
||||
fun Route.getCitizenVote(repo: VoteRepository, ac: VoteAccessControl) {
|
||||
get<CitizenVotesRequest> {
|
||||
mustBeAuth()
|
||||
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
|
||||
if (votes.isNotEmpty()) {
|
||||
ac.assert { canView(votes, citizenOrNull) }
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.vote.routes
|
||||
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.vote.VoteAccessControl
|
||||
import fr.dcproject.component.vote.database.VoteArticleRepository
|
||||
@@ -31,6 +32,7 @@ object GetCitizenVotesOnArticle {
|
||||
|
||||
fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) {
|
||||
get<CitizenVoteArticleRequest> {
|
||||
mustBeAuth()
|
||||
val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
|
||||
ac.assert { canView(votes.result, citizenOrNull) }
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import fr.dcproject.component.article.database.ArticleRef
|
||||
import fr.dcproject.component.article.database.ArticleRepository
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.vote.VoteAccessControl
|
||||
import fr.dcproject.component.vote.database.VoteArticleRepository
|
||||
import fr.dcproject.component.vote.database.VoteForUpdate
|
||||
@@ -29,6 +30,7 @@ object PutVoteOnArticle {
|
||||
|
||||
fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) {
|
||||
put<ArticleVoteRequest> {
|
||||
mustBeAuth()
|
||||
val input = call.receiveOrBadRequest<ArticleVoteRequest.Input>()
|
||||
val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
|
||||
val vote = VoteForUpdate(
|
||||
|
||||
@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.comment.generic.database.CommentRepository
|
||||
import fr.dcproject.component.vote.VoteAccessControl
|
||||
import fr.dcproject.component.vote.database.VoteCommentRepository
|
||||
@@ -26,6 +27,7 @@ object PutVoteOnComment {
|
||||
|
||||
fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, ac: VoteAccessControl) {
|
||||
put<CommentVoteRequest> {
|
||||
mustBeAuth()
|
||||
val comment = commentRepo.findById(it.comment)!!
|
||||
val content = call.receiveOrBadRequest<CommentVoteRequest.Content>()
|
||||
val vote = VoteForUpdate(
|
||||
|
||||
@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||
import fr.dcproject.component.constitution.database.ConstitutionRepository
|
||||
import fr.dcproject.component.vote.VoteAccessControl
|
||||
@@ -30,6 +31,7 @@ object PutVoteOnConstitution {
|
||||
|
||||
fun Route.voteConstitution(repo: VoteConstitutionRepository, ac: VoteAccessControl, constitutionRepo: ConstitutionRepository) {
|
||||
put<ConstitutionVoteRequest> {
|
||||
mustBeAuth()
|
||||
val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}")
|
||||
val content = call.receiveOrBadRequest<Input>()
|
||||
val vote = VoteForUpdate(
|
||||
|
||||
@@ -5,6 +5,7 @@ import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizen
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.workgroup.WorkgroupAccessControl
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupForUpdate
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupRepository
|
||||
@@ -33,6 +34,7 @@ object CreateWorkgroup {
|
||||
|
||||
fun Route.createWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
|
||||
post<PostWorkgroupRequest> {
|
||||
mustBeAuth()
|
||||
call.receiveOrBadRequest<Input>().run {
|
||||
WorkgroupForUpdate(
|
||||
id ?: UUID.randomUUID(),
|
||||
|
||||
@@ -2,6 +2,7 @@ package fr.dcproject.component.workgroup.routes
|
||||
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.workgroup.WorkgroupAccessControl
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupRepository
|
||||
import io.ktor.application.call
|
||||
@@ -20,6 +21,7 @@ object DeleteWorkgroup {
|
||||
|
||||
fun Route.deleteWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
|
||||
delete<DeleteWorkgroupRequest> {
|
||||
mustBeAuth()
|
||||
repo.findById(it.workgroupId)?.let { workgroup ->
|
||||
ac.assert { canDelete(workgroup, citizenOrNull) }
|
||||
repo.delete(workgroup)
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.workgroup.WorkgroupAccessControl
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupForUpdate
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupRepository
|
||||
@@ -31,6 +32,7 @@ object EditWorkgroup {
|
||||
|
||||
fun Route.editWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
|
||||
put<PutWorkgroupRequest> {
|
||||
mustBeAuth()
|
||||
repo.findById(it.workgroupId)?.let { old ->
|
||||
call.receiveOrBadRequest<Input>().run {
|
||||
WorkgroupForUpdate(
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.citizen.database.CitizenRef
|
||||
import fr.dcproject.component.workgroup.WorkgroupAccessControl
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupRepository
|
||||
@@ -44,6 +45,7 @@ object AddMemberToWorkgroup {
|
||||
fun Route.addMemberToWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
|
||||
/* Add members to workgroup */
|
||||
post<WorkgroupsMembersRequest> {
|
||||
mustBeAuth()
|
||||
repo.findById(it.workgroupId)?.let { workgroup ->
|
||||
call.getMembersFromRequest().let { members ->
|
||||
ac.assert { canAddMembers(workgroup, citizenOrNull) }
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.citizen.database.CitizenRef
|
||||
import fr.dcproject.component.workgroup.WorkgroupAccessControl
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupRepository
|
||||
@@ -35,6 +36,7 @@ object DeleteMembersOfWorkgroup {
|
||||
fun Route.deleteMemberOfWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
|
||||
/* Delete members of workgroup */
|
||||
delete<WorkgroupsMembersRequest> {
|
||||
mustBeAuth()
|
||||
repo.findById(it.workgroupId)?.let { workgroup ->
|
||||
call.getMembersFromRequest()
|
||||
.let { members ->
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members
|
||||
import fr.dcproject.common.security.assert
|
||||
import fr.dcproject.common.utils.receiveOrBadRequest
|
||||
import fr.dcproject.component.auth.citizenOrNull
|
||||
import fr.dcproject.component.auth.mustBeAuth
|
||||
import fr.dcproject.component.citizen.database.CitizenRef
|
||||
import fr.dcproject.component.workgroup.WorkgroupAccessControl
|
||||
import fr.dcproject.component.workgroup.database.WorkgroupRepository
|
||||
@@ -42,6 +43,7 @@ object UpdateMemberOfWorkgroup {
|
||||
fun Route.updateMemberOfWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
|
||||
/* Update members of workgroup */
|
||||
put<WorkgroupsMembersRequest> {
|
||||
mustBeAuth()
|
||||
repo.findById(it.workgroupId)?.let { workgroup ->
|
||||
call.getMembersFromRequest().let { members ->
|
||||
ac.assert { canUpdateMembers(workgroup, citizenOrNull) }
|
||||
|
||||
@@ -42,3 +42,11 @@ mail {
|
||||
key = ${?SEND_GRID_KEY}
|
||||
}
|
||||
}
|
||||
|
||||
jwt {
|
||||
secret = ${?JWT_SECRET}
|
||||
issuer = "dc-project.fr"
|
||||
issuer = ${?JWT_ISSUER}
|
||||
validity = 36000000
|
||||
validity = ${?JWT_VALIDITY}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@ begin
|
||||
case direction when 'asc' then
|
||||
case sort
|
||||
when 'title' then a.title
|
||||
when 'created_at' then a.created_at::text
|
||||
when 'createdAt' then a.created_at::text
|
||||
when 'vote' then ca.score::text
|
||||
when 'popularity' then ca.total::text
|
||||
else null
|
||||
@@ -54,7 +54,7 @@ begin
|
||||
case direction when 'desc' then
|
||||
case sort
|
||||
when 'title' then a.title
|
||||
when 'created_at' then a.created_at::text
|
||||
when 'createdAt' then a.created_at::text
|
||||
when 'vote' then ca.score::text
|
||||
when 'popularity' then ca.total::text
|
||||
end
|
||||
|
||||
@@ -23,14 +23,14 @@ begin
|
||||
case direction when 'asc' then
|
||||
case sort
|
||||
when 'name' then (z.name->'first_name')::text
|
||||
when 'created_at' then z.created_at::text
|
||||
when 'createdAt' then z.created_at::text
|
||||
else null
|
||||
end
|
||||
end,
|
||||
case direction when 'desc' then
|
||||
case sort
|
||||
when 'name' then (z.name->'first_name')::text
|
||||
when 'created_at' then z.created_at::text
|
||||
when 'createdAt' then z.created_at::text
|
||||
end
|
||||
end
|
||||
desc,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
create or replace function comment(reference regclass, resource json, out _id uuid)
|
||||
create or replace function comment(reference regclass, inout resource json)
|
||||
language plpgsql as
|
||||
$$
|
||||
declare
|
||||
@@ -17,7 +17,8 @@ begin
|
||||
else
|
||||
raise exception 'comment with target as "%", is not implemented', reference::text;
|
||||
end if;
|
||||
_id = _new_id;
|
||||
|
||||
select find_comment_by_id(_new_id) into resource;
|
||||
end;
|
||||
$$;
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
create or replace function edit_comment(_id uuid, _content text) returns void
|
||||
create or replace function edit_comment(_id uuid, _content text, out resource json)
|
||||
language plpgsql as
|
||||
$$
|
||||
begin
|
||||
update comment c set
|
||||
"content" = _content
|
||||
where c.id = _id;
|
||||
|
||||
select find_comment_by_id(_id) into resource;
|
||||
end;
|
||||
$$;
|
||||
|
||||
@@ -26,7 +26,7 @@ begin
|
||||
else null
|
||||
end desc,
|
||||
case sort
|
||||
when 'created_at' then com.created_at::text
|
||||
when 'createdAt' then com.created_at::text
|
||||
else null
|
||||
end desc,
|
||||
com.created_at desc
|
||||
|
||||
@@ -22,14 +22,14 @@ begin
|
||||
case direction when 'asc' then
|
||||
case sort
|
||||
when 'title' then c.title
|
||||
when 'created_at' then c.created_at::text
|
||||
when 'createdAt' then c.created_at::text
|
||||
else null
|
||||
end
|
||||
end,
|
||||
case direction when 'desc' then
|
||||
case sort
|
||||
when 'title' then c.title
|
||||
when 'created_at' then c.created_at::text
|
||||
when 'createdAt' then c.created_at::text
|
||||
end
|
||||
end
|
||||
desc,
|
||||
|
||||
@@ -21,7 +21,7 @@ begin
|
||||
f.created_at,
|
||||
f.target_reference,
|
||||
json_build_object('id', f.target_id) as target,
|
||||
json_build_object('id', f.created_by_id) as created_by
|
||||
find_citizen_by_id_with_user(f.created_by_id) as created_by
|
||||
from follow_article as f
|
||||
join article a on f.target_id = a.id
|
||||
where a.version_id = _version_id
|
||||
|
||||
7
src/test/kotlin/assert/Range.kt
Normal file
7
src/test/kotlin/assert/Range.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package assert
|
||||
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
infix fun IntProgression.assertContain(expected: Int) {
|
||||
assertTrue(this.contains(expected), "Expected $this less than $expected")
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import org.koin.test.get
|
||||
@KtorExperimentalLocationsAPI
|
||||
@KtorExperimentalAPI
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@Tags(Tag("functional"))
|
||||
@Tags(Tag("functional"), Tag("mail"))
|
||||
class MailerTest : KoinTest, AutoCloseKoinTest() {
|
||||
@InternalCoroutinesApi
|
||||
@ExperimentalCoroutinesApi
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.junit.jupiter.api.TestInstance
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
|
||||
@Tags(Tag("functional"))
|
||||
@Tags(Tag("functional"), Tag("notification"))
|
||||
class NotificationConsumerTest {
|
||||
companion object {
|
||||
@BeforeAll
|
||||
|
||||
@@ -24,13 +24,13 @@ import org.junit.jupiter.api.Tags
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@Tags(Tag("functional"))
|
||||
@Tags(Tag("functional"), Tag("notification"))
|
||||
internal class NotificationsPushTest {
|
||||
companion object {
|
||||
@BeforeAll
|
||||
@JvmStatic
|
||||
fun before() {
|
||||
val config: Configuration = Configuration("application-test.conf")
|
||||
val config = Configuration("application-test.conf")
|
||||
RedisClient.create(config.redis).connect().sync().flushall()
|
||||
|
||||
/* Purge rabbit notification queues */
|
||||
@@ -45,7 +45,7 @@ internal class NotificationsPushTest {
|
||||
|
||||
@Test
|
||||
fun `Notification from redis is well catch and return`() = runBlocking {
|
||||
val config: Configuration = Configuration("application-test.conf")
|
||||
val config = Configuration("application-test.conf")
|
||||
/* Redis client for test */
|
||||
val redisClientTest = RedisClient.create(config.redis)
|
||||
|
||||
@@ -74,7 +74,7 @@ internal class NotificationsPushTest {
|
||||
}
|
||||
val notifAfterSubscribe = ArticleUpdateNotification(article)
|
||||
|
||||
/* init event for emulate incomint message from websocket */
|
||||
/* init event for emulate incoming message from websocket */
|
||||
val event = MutableSharedFlow<Notification>()
|
||||
val incomingFlow = event.asSharedFlow()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.junit.jupiter.api.TestInstance
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@Tags(Tag("functional"))
|
||||
@Tags(Tag("functional"), Tag("utils"))
|
||||
class ResourcesKtTest {
|
||||
@Test
|
||||
fun readResource() {
|
||||
|
||||
@@ -2,8 +2,8 @@ package functional
|
||||
|
||||
import fr.dcproject.application.Env.TEST
|
||||
import fr.dcproject.application.module
|
||||
import fr.dcproject.component.article.ArticleViewManager
|
||||
import fr.dcproject.component.article.database.ArticleForView
|
||||
import fr.dcproject.component.article.database.ArticleViewRepository
|
||||
import fr.dcproject.component.auth.database.UserCreator
|
||||
import fr.dcproject.component.citizen.database.CitizenCreator
|
||||
import fr.dcproject.component.citizen.database.CitizenI
|
||||
@@ -25,7 +25,7 @@ import java.util.UUID
|
||||
@KtorExperimentalAPI
|
||||
@ExperimentalCoroutinesApi
|
||||
@TestInstance(PER_CLASS)
|
||||
@Tags(Tag("functional"))
|
||||
@Tags(Tag("functional"), Tag("view"))
|
||||
class ViewTest {
|
||||
@Test
|
||||
fun `test View Article`() {
|
||||
@@ -44,33 +44,33 @@ class ViewTest {
|
||||
val citizenRef = CitizenRef()
|
||||
|
||||
withTestApplication({ module(TEST) }) {
|
||||
val viewManager: ArticleViewManager<ArticleForView> = application.get()
|
||||
val viewRepository: ArticleViewRepository<ArticleForView> = application.get()
|
||||
|
||||
/* Get view before */
|
||||
val startView = viewManager.getViewsCount(article)
|
||||
val startView = viewRepository.getViewsCount(article)
|
||||
|
||||
/* Add View */
|
||||
viewManager.addView(
|
||||
viewRepository.addView(
|
||||
"1.2.3.4",
|
||||
article,
|
||||
citizenRef
|
||||
)
|
||||
|
||||
/* Add View */
|
||||
viewManager.addView(
|
||||
viewRepository.addView(
|
||||
"10.10.10.10",
|
||||
article,
|
||||
citizenRef
|
||||
)
|
||||
|
||||
/* Add View */
|
||||
viewManager.addView(
|
||||
viewRepository.addView(
|
||||
"8.8.8.8",
|
||||
article
|
||||
)
|
||||
|
||||
/* Add View */
|
||||
viewManager.addView(
|
||||
viewRepository.addView(
|
||||
"1.1.1.1",
|
||||
article
|
||||
)
|
||||
@@ -79,7 +79,7 @@ class ViewTest {
|
||||
Thread.sleep(1000)
|
||||
|
||||
/* Get view */
|
||||
val afterView = viewManager.getViewsCount(article)
|
||||
val afterView = viewRepository.getViewsCount(article)
|
||||
|
||||
/* Check if view has increment */
|
||||
afterView.total `should be equal to` startView.total + 4
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
package integration
|
||||
|
||||
import integration.steps.then.`And have property`
|
||||
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.`Then the response should be`
|
||||
import fr.dcproject.common.utils.toUUID
|
||||
import integration.steps.`when`.Validate
|
||||
import integration.steps.`when`.`When I send a GET request`
|
||||
import integration.steps.`when`.`When I send a POST request`
|
||||
import integration.steps.`when`.`with body`
|
||||
import integration.steps.then.`whish contains`
|
||||
import integration.steps.then.and
|
||||
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.Forbidden
|
||||
import io.ktor.http.HttpStatusCode.Companion.NotFound
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
import org.junit.jupiter.api.Tag
|
||||
import org.junit.jupiter.api.Tags
|
||||
@@ -30,13 +36,24 @@ class `Article routes` : BaseTest() {
|
||||
fun `I can get article list`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have articles`(3)
|
||||
`When I send a GET request`("/articles") `Then the response should be` OK and {
|
||||
`Given I have article`(createdBy = "ddb17f17-e8ab-4ada-bdf7-bfd6b0f1b5ed".toUUID())
|
||||
`When I send a GET request`("/articles?page=1&limit=10&sort=title&createdBy=ddb17f17-e8ab-4ada-bdf7-bfd6b0f1b5ed") `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain pattern`("$.result[0].createdBy.name.firstName", "firstName.+")
|
||||
`And the response should contain pattern`("$.result[1].createdBy.name.firstName", "firstName.+")
|
||||
`And the response should contain pattern`("$.result[2].createdBy.name.firstName", "firstName.+")
|
||||
`And the response should not contain`("$.result[3]")
|
||||
`And the response should contain list`("$.result", 3, 3)
|
||||
`And the response should not contain`("$.result[1]")
|
||||
`And the response should contain list`("$.result", 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot get article list`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have articles`(3)
|
||||
`When I send a GET request`("/articles?page=1&limit=10&sort=title&createdBy=hello", Validate.ALL - Validate.REQUEST_PARAM) `Then the response should be` BadRequest and {
|
||||
`And the response should contain`("$.invalidParams[*].name", ".createdBy")
|
||||
`And the response should contain`("$.invalidParams[*].reason", "must be UUID")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,8 +66,8 @@ class `Article routes` : BaseTest() {
|
||||
`Given I have article created by workgroup`("2bccd5a7-9082-4b31-88f8-e25d70b22b12")
|
||||
`When I send a GET request`("/articles?workgroup=2bccd5a7-9082-4b31-88f8-e25d70b22b12") `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.total") `whish contains` 1
|
||||
`And have property`("$.result[0]workgroup.name") `whish contains` "Les papy"
|
||||
`And have property`("$.total") `which contains` 1
|
||||
`And have property`("$.result[0]workgroup.name") `which contains` "Les papy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +78,31 @@ class `Article routes` : BaseTest() {
|
||||
`Given I have article`(id = "65cda9f3-8991-4420-8d41-1da9da72c9bb")
|
||||
`When I send a GET request`("/articles/65cda9f3-8991-4420-8d41-1da9da72c9bb") `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.id") `whish contains` "65cda9f3-8991-4420-8d41-1da9da72c9bb"
|
||||
`And have property`("$.id") `which contains` "65cda9f3-8991-4420-8d41-1da9da72c9bb"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I cannot get article with id doesn't exist`() {
|
||||
withIntegrationApplication {
|
||||
`When I send a GET request`("/articles/635fe2e8-2dbc-4c80-b306-101d38a4ab23") `Then the response should be` NotFound and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.title", "Article 635fe2e8-2dbc-4c80-b306-101d38a4ab23 not found")
|
||||
`And the response should contain`("$.statusCode", 404)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot get article by id with wrong id format`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have article`(id = "65cda9f3-8991-4420-8d41-1da9da72c9bb")
|
||||
`When I send a GET request`("/articles/abcd") `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[0].name", "ID")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must be UUID")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,10 +111,36 @@ class `Article routes` : BaseTest() {
|
||||
fun `I can get versions of article by the id`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have article`(id = "13e6091c-8fed-4600-b079-a97a6b7a9800")
|
||||
`When I send a GET request`("/articles/13e6091c-8fed-4600-b079-a97a6b7a9800/versions") `Then the response should be` OK and {
|
||||
`When I send a GET request`("/articles/13e6091c-8fed-4600-b079-a97a6b7a9800/versions?page=1&limit=10&sort=title") `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.total") `whish contains` 1
|
||||
`And have property`("$.result[0].id") `whish contains` "13e6091c-8fed-4600-b079-a97a6b7a9800"
|
||||
`And have property`("$.total") `which contains` 1
|
||||
`And have property`("$.result[0].id") `which contains` "13e6091c-8fed-4600-b079-a97a6b7a9800"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot get versions of article by the id with wrong id`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have article`(id = "13e6091c-8fed-4600-b079-a97a6b7a9800")
|
||||
`When I send a GET request`("/articles/abcd/versions") `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[0].name", ".article")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must be UUID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot get versions of article by the id with wrong request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have article`(id = "13e6091c-8fed-4600-b079-a97a6b7a9800")
|
||||
`When I send a GET request`("/articles/13e6091c-8fed-4600-b079-a97a6b7a9800/versions?page=1&limit=10&sort=wrong") `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[0].name", ".sort")
|
||||
`And the response should contain pattern`("$.invalidParams[0].reason", "must be one of: ('[^']+'(, )?)+")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +151,63 @@ class `Article routes` : BaseTest() {
|
||||
`Given I have citizen`("John", "Doe")
|
||||
`When I send a POST request`("/articles") {
|
||||
`authenticated as`("John", "Doe")
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"versionId": "09c418b6-63ba-448b-b38b-502b41cd500e",
|
||||
"title": "title2",
|
||||
"anonymous": false,
|
||||
"content": "Sed malesuada ante et sem congue, scelerisque feugiat lorem viverra.",
|
||||
"description": "Sed vulputate, ligula id porta posuere, sapien lorem mattis arcu, sit amet luctus erat orci sed tellus.",
|
||||
"tags": [
|
||||
"green"
|
||||
]
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.versionId") `which contains` "09c418b6-63ba-448b-b38b-502b41cd500e"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("Forbidden")
|
||||
fun `I cannot create an article if I'm not connected`() {
|
||||
withIntegrationApplication {
|
||||
`When I send a POST request`("/articles") {
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"versionId": "e3c7ce42-241c-4caf-9a59-aba4e466440e",
|
||||
"title": "title2",
|
||||
"anonymous": false,
|
||||
"content": "Sed malesuada ante et sem congue, scelerisque feugiat lorem viverra.",
|
||||
"description": "Sed vulputate, ligula id porta posuere, sapien lorem mattis arcu, sit amet luctus erat orci sed tellus.",
|
||||
"tags": [
|
||||
"green"
|
||||
]
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` Forbidden and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.statusCode", 403)
|
||||
`And the response should contain`("$.title", "No User Connected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot create an article with wrong request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("John", "Doe")
|
||||
`When I send a POST request`("/articles", Validate.NONE) {
|
||||
`authenticated as`("John", "Doe")
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"versionId": "09c418b6-63ba-448b-b38b-502b41cd500e",
|
||||
"title": "title2",
|
||||
@@ -95,10 +218,14 @@ class `Article routes` : BaseTest() {
|
||||
"green"
|
||||
]
|
||||
}
|
||||
""")
|
||||
} `Then the response should be` OK and {
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.versionId") `whish contains` "09c418b6-63ba-448b-b38b-502b41cd500e"
|
||||
`And the response should contain`("$.invalidParams[0].name", ".content")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must have at least 50 characters")
|
||||
`And the response should contain`("$.invalidParams[1].name", ".description")
|
||||
`And the response should contain`("$.invalidParams[1].reason", "must have at least 50 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
147
src/test/kotlin/integration/Check auth on all routes.kt
Normal file
147
src/test/kotlin/integration/Check auth on all routes.kt
Normal file
@@ -0,0 +1,147 @@
|
||||
package integration
|
||||
|
||||
import fr.dcproject.common.utils.getResource
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import org.junit.jupiter.api.Tag
|
||||
import org.junit.jupiter.api.Tags
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.openapi4j.core.model.OAIContext
|
||||
import org.openapi4j.parser.OpenApi3Parser
|
||||
import org.openapi4j.parser.model.v3.OpenApi3
|
||||
import org.openapi4j.parser.model.v3.Operation
|
||||
import org.openapi4j.parser.model.v3.Parameter
|
||||
import org.openapi4j.parser.model.v3.Path
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@Tags(Tag("integration"), Tag("auth"))
|
||||
class `Check auth on all routes` : BaseTest() {
|
||||
@Test
|
||||
fun `Check all routes`() {
|
||||
val filePath = "/openapi.yaml"
|
||||
OpenApi3Parser().parse(File(filePath.getResource().toURI()), true).let { api: OpenApi3 ->
|
||||
/* Loop on paths and http methods */
|
||||
api.paths.flatMap { (pathName: String, path: Path) ->
|
||||
path.operations
|
||||
/* Take only the secure route */
|
||||
.filter { (_, operation: Operation) -> operation.hasSecurityRequirements() }
|
||||
.map { (methodName, _) ->
|
||||
/* Send request to check security */
|
||||
sendRequest(
|
||||
path.buildUrl(pathName, methodName, api.context), /* Replace route to real URL */
|
||||
HttpMethod.parse(methodName.toUpperCase()) /* Convert http method name to enum */
|
||||
)
|
||||
}
|
||||
}.let { requests ->
|
||||
/* Check security of routes */
|
||||
assertTrue(
|
||||
requests.all { it.statusCode == HttpStatusCode.Forbidden },
|
||||
requests
|
||||
.filter { it.statusCode != HttpStatusCode.Forbidden }
|
||||
.joinToString("\n") { it.toString() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendRequest(uri: String, method: HttpMethod): RequestResponse {
|
||||
return try {
|
||||
withIntegrationApplication {
|
||||
handleRequest(true) {
|
||||
this.method = method
|
||||
this.uri = uri
|
||||
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
addHeader(HttpHeaders.Accept, ContentType.Application.Json.toString())
|
||||
}.run {
|
||||
RequestResponse(
|
||||
response.status() ?: error("Request error"),
|
||||
method,
|
||||
uri
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
RequestResponse(
|
||||
HttpStatusCode.InternalServerError,
|
||||
method,
|
||||
uri
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class RequestResponse(
|
||||
val statusCode: HttpStatusCode,
|
||||
val method: HttpMethod,
|
||||
val uri: String
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return """HttpStatus ${statusCode.value} for: ${method.value.padStart(6, ' ')} $uri"""
|
||||
}
|
||||
}
|
||||
|
||||
private fun Path.buildUrl(path: String, methodName: String, context: OAIContext): String {
|
||||
val urlReplaced = this.getParametersIn(context, "path")
|
||||
.fold(path) { pathToReplace: String, parameter: Parameter ->
|
||||
"""\{${parameter.name}}""".toRegex().replace(
|
||||
pathToReplace,
|
||||
parameter.generateFakeValue()
|
||||
)
|
||||
}
|
||||
|
||||
val rootQueryParameters = this.getParametersIn(context, "query")
|
||||
.filter { it.isRequired }
|
||||
.map { parameter ->
|
||||
parameter
|
||||
.generateFakeArray()
|
||||
.joinToString("&") { "${parameter.name}=$it" }
|
||||
}
|
||||
|
||||
val queryParameters = this.getOperation(methodName).getParametersIn(context, "query")
|
||||
.filter { it.isRequired }
|
||||
.map { parameter ->
|
||||
parameter
|
||||
.generateFakeArray()
|
||||
.joinToString("&") { "${parameter.name}=$it" }
|
||||
}
|
||||
val allParameters: String = (rootQueryParameters + queryParameters)
|
||||
.joinToString("&")
|
||||
.let {
|
||||
if (it.isNotEmpty()) {
|
||||
"?$it"
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return "$urlReplaced$allParameters"
|
||||
}
|
||||
|
||||
private fun Parameter.generateFakeValue(): String {
|
||||
return if (example != null) {
|
||||
example.toString()
|
||||
} else if (schema.type == "string" && schema.format == "uuid") {
|
||||
UUID.randomUUID().toString()
|
||||
} else {
|
||||
"example123"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Parameter.generateFakeArray(): List<String> {
|
||||
if (schema.type != "array") {
|
||||
error("Parameter is not an array")
|
||||
}
|
||||
return if (example != null && example is Iterable<*>) {
|
||||
(example as Iterable<*>).map { it.toString() }
|
||||
} else if (schema.itemsSchema.type == "string" && schema.itemsSchema.format == "uuid") {
|
||||
listOf(UUID.randomUUID().toString())
|
||||
} else {
|
||||
listOf("example123")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package integration
|
||||
|
||||
import integration.steps.`when`.Validate
|
||||
import integration.steps.then.`And have property`
|
||||
import integration.steps.then.`And the response should not be null`
|
||||
import integration.steps.then.`Then the response should be`
|
||||
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.then.`whish contains`
|
||||
import integration.steps.then.and
|
||||
import integration.steps.given.`Given I have citizen`
|
||||
import integration.steps.given.`authenticated as`
|
||||
import integration.steps.then.`And have property`
|
||||
import integration.steps.then.`And the response should not be null`
|
||||
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.Created
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
@@ -26,7 +26,7 @@ class `Citizen routes` : BaseTest() {
|
||||
fun `I can get Citizens information`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Jean", "Perrin", id = "5267a5c6-af42-4a02-aa2b-6b71d2e43973")
|
||||
`When I send a GET request`("/citizens") {
|
||||
`When I send a GET request`("/citizens?page=1&limit=5&sort=createdAt") {
|
||||
`authenticated as`("Jean", "Perrin")
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
@@ -34,6 +34,19 @@ class `Citizen routes` : BaseTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot get Citizens information with wrong request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Jean", "Perrin", id = "5267a5c6-af42-4a02-aa2b-6b71d2e43973")
|
||||
`When I send a GET request`("/citizens?page=1&limit=5&sort=created_at", Validate.ALL - Validate.REQUEST_PARAM) {
|
||||
`authenticated as`("Jean", "Perrin")
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I can get specific Citizen information`() {
|
||||
withIntegrationApplication {
|
||||
@@ -42,7 +55,7 @@ class `Citizen routes` : BaseTest() {
|
||||
`authenticated as`("Linus", "Pauling")
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.id") `whish contains` "47a05c0f-7329-46c3-a7d0-325db37e9114"
|
||||
`And have property`("$.id") `which contains` "47a05c0f-7329-46c3-a7d0-325db37e9114"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +68,7 @@ class `Citizen routes` : BaseTest() {
|
||||
`authenticated as`("Henri", "Becquerel")
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.id") `whish contains` "47356809-c8ef-4649-8b99-1c5cb9886d38"
|
||||
`And have property`("$.id") `which contains` "47356809-c8ef-4649-8b99-1c5cb9886d38"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,28 +79,33 @@ class `Citizen routes` : BaseTest() {
|
||||
`Given I have citizen`("Georges", "Charpak", id = "0c966522-4071-43e5-a3ca-cfff2557f2cf")
|
||||
`When I send a PUT request`("/citizens/0c966522-4071-43e5-a3ca-cfff2557f2cf/password/change") {
|
||||
`authenticated as`("Georges", "Charpak")
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"oldPassword": "azerty",
|
||||
"newPassword": "qwerty"
|
||||
"oldPassword": "Azerty123!",
|
||||
"newPassword": "Qwerty123!"
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` Created
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot change my password if request is bad formatted`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Louis", "Breguet", id = "6cf2a19d-d15d-4ee5-b2a9-907afd26b525")
|
||||
`When I send a PUT request`("/citizens/6cf2a19d-d15d-4ee5-b2a9-907afd26b525/password/change", Validate.ALL - Validate.REQUEST_BODY) {
|
||||
`authenticated as`("Louis", "Breguet")
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"plup": "azerty",
|
||||
"gloup": "qwerty"
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` BadRequest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package integration
|
||||
|
||||
import fr.dcproject.component.citizen.database.CitizenI.Name
|
||||
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.`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`.`When I send a PUT request`
|
||||
import integration.steps.`when`.`with body`
|
||||
import integration.steps.then.and
|
||||
import integration.steps.given.`Given I have article`
|
||||
import integration.steps.given.`Given I have citizen`
|
||||
import integration.steps.given.`Given I have comment on article`
|
||||
import integration.steps.given.`authenticated as`
|
||||
import integration.steps.then.`And the response should contain`
|
||||
import integration.steps.then.`And the response should not be null`
|
||||
import integration.steps.then.`Then the response should be`
|
||||
import integration.steps.then.and
|
||||
import io.ktor.http.HttpStatusCode.Companion.BadRequest
|
||||
import io.ktor.http.HttpStatusCode.Companion.Created
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
import org.junit.jupiter.api.Tag
|
||||
@@ -30,15 +33,40 @@ class `Comment articles routes` : BaseTest() {
|
||||
`Given I have article`(id = "aa16c635-28da-46f0-9a89-934eef88c7ca")
|
||||
`When I send a POST request`("/articles/aa16c635-28da-46f0-9a89-934eef88c7ca/comments") {
|
||||
`authenticated as`("Michael", "Faraday")
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"content": "Hello mister"
|
||||
"content": "Hello mister MARABOUTCHA"
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` Created and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.target.id", "aa16c635-28da-46f0-9a89-934eef88c7ca")
|
||||
`And the response should contain`("$.content", "Hello mister")
|
||||
`And the response should contain`("$.content", "Hello mister MARABOUTCHA")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot comment article with bad request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Michael", "Faraday")
|
||||
`Given I have article`(id = "aa16c635-28da-46f0-9a89-934eef88c7ca")
|
||||
`When I send a POST request`("/articles/aa16c635-28da-46f0-9a89-934eef88c7ca/comments", ALL - REQUEST_BODY) {
|
||||
`authenticated as`("Michael", "Faraday")
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"content": "To small content"
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[0].name", ".content")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +78,7 @@ class `Comment articles routes` : BaseTest() {
|
||||
`Given I have citizen`("Enrico", "Fermi")
|
||||
`Given I have article`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
|
||||
`Given I have comment on article`(article = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
|
||||
`When I send a GET request`("/articles/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments") {
|
||||
`When I send a GET request`("/articles/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=votes") {
|
||||
`authenticated as`("Enrico", "Fermi")
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
@@ -59,6 +87,23 @@ class `Comment articles routes` : BaseTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot get all comment on article with wrong parameters`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Enrico", "Fermi")
|
||||
`Given I have article`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
|
||||
`Given I have comment on article`(article = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
|
||||
`When I send a GET request`("/articles/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=wrong", ALL - REQUEST_PARAM) {
|
||||
`authenticated as`("Enrico", "Fermi")
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[*].name", ".sort")
|
||||
`And the response should contain`("$.invalidParams[*].reason", "must be one of: 'votes', 'createdAt'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO add votes */
|
||||
@Test
|
||||
fun `I can get all comment on article sorted by votes`() {
|
||||
@@ -82,6 +127,7 @@ class `Comment articles routes` : BaseTest() {
|
||||
`Given I have article`(id = "17df7fb9-b388-4e20-ab19-29c29972da01", createdBy = Name("Erwin", "Schrodinger"))
|
||||
`Given I have comment on article`(article = "17df7fb9-b388-4e20-ab19-29c29972da01", createdBy = Name("Erwin", "Schrodinger"))
|
||||
`When I send a GET request`("/citizens/292a20cc-4a60-489e-9866-a95d38ffaf47/comments/articles") {
|
||||
`authenticated as`("Erwin", "Schrodinger")
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.currentPage", 1)
|
||||
@@ -90,43 +136,4 @@ class `Comment articles routes` : BaseTest() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I can edit comment`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Hubert", "Reeves")
|
||||
`Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1")
|
||||
`Given I have comment on article`(article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1", createdBy = Name("Hubert", "Reeves"), id = "fd30d20f-656c-42c6-8955-f61c04537464")
|
||||
`When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464") {
|
||||
`authenticated as`("Hubert", "Reeves")
|
||||
`with body`("""
|
||||
{
|
||||
"content": "Hello boy"
|
||||
}
|
||||
""")
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.content", "Hello boy")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I can get comment by its ID`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Alfred", "Kastler")
|
||||
`Given I have article`(id = "3897465b-19d2-43a0-86ea-1e29dbb11ec9")
|
||||
`Given I have comment on article`(
|
||||
article = "3897465b-19d2-43a0-86ea-1e29dbb11ec9",
|
||||
createdBy = Name("Alfred", "Kastler"),
|
||||
id = "edd296a8-fc7a-4717-a2bb-9f035ceca3c2",
|
||||
content = "Hello boy"
|
||||
)
|
||||
`When I send a GET request`("/comments/edd296a8-fc7a-4717-a2bb-9f035ceca3c2") {
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.content", "Hello boy")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package integration
|
||||
|
||||
import fr.dcproject.component.citizen.database.CitizenI.Name
|
||||
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.`when`.Validate
|
||||
import integration.steps.`when`.Validate.ALL
|
||||
import integration.steps.`when`.Validate.REQUEST_BODY
|
||||
import integration.steps.`when`.`When I send a GET request`
|
||||
import integration.steps.`when`.`When I send a POST request`
|
||||
import integration.steps.`when`.`with body`
|
||||
import integration.steps.then.and
|
||||
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.Created
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
import org.junit.jupiter.api.Tag
|
||||
@@ -30,13 +34,72 @@ class `Comment constitutions routes` : BaseTest() {
|
||||
`Given I have constitution`(id = "1707c287-a472-4a62-89f2-9e85030e915c")
|
||||
`When I send a POST request`("/constitutions/1707c287-a472-4a62-89f2-9e85030e915c/comments") {
|
||||
`authenticated as`("Nicolas", "Copernic")
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"content": "Hello mister"
|
||||
"content": "Hello mister MARABOUTCHA"
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` Created and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.target.id", "1707c287-a472-4a62-89f2-9e85030e915c")
|
||||
`And the response should contain`("$.content", "Hello mister MARABOUTCHA")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot comment constitution with bad request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Nicolas", "Copernic")
|
||||
`Given I have constitution`(id = "aa16c635-28da-46f0-9a89-934eef88c7ca")
|
||||
`When I send a POST request`("/constitutions/aa16c635-28da-46f0-9a89-934eef88c7ca/comments", ALL - REQUEST_BODY) {
|
||||
`authenticated as`("Nicolas", "Copernic")
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"content": "To small content"
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[0].name", ".content")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I can get all comment on constitution`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Enrico", "Fermi")
|
||||
`Given I have constitution`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
|
||||
`Given I have comment on constitution`(constitution = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
|
||||
`When I send a GET request`("/constitutions/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=votes") {
|
||||
`authenticated as`("Enrico", "Fermi")
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.result[0].target.id", "6166c078-ca97-4366-b0aa-2a5cd558c78a")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot get all comment on constitution with wrong parameters`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Enrico", "Fermi")
|
||||
`Given I have constitution`(id = "6166c078-ca97-4366-b0aa-2a5cd558c78a")
|
||||
`Given I have comment on constitution`(constitution = "6166c078-ca97-4366-b0aa-2a5cd558c78a", createdBy = Name("Enrico", "Fermi"))
|
||||
`When I send a GET request`("/constitutions/6166c078-ca97-4366-b0aa-2a5cd558c78a/comments?page=1&limit=40&sort=wrong", ALL - Validate.REQUEST_PARAM) {
|
||||
`authenticated as`("Enrico", "Fermi")
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[*].name", ".sort")
|
||||
`And the response should contain`("$.invalidParams[*].reason", "must be one of: 'votes', 'createdAt'")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,13 +111,14 @@ class `Comment constitutions routes` : BaseTest() {
|
||||
`Given I have constitution`(id = "34ddd50a-da00-4a90-a869-08baa2a121be", createdBy = Name("Charles", "Darwin"))
|
||||
`Given I have comment on constitution`(constitution = "34ddd50a-da00-4a90-a869-08baa2a121be", createdBy = Name("Charles", "Darwin"))
|
||||
`When I send a GET request`("/citizens/46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5/comments/constitutions") {
|
||||
`authenticated as`("Charles", "Darwin")
|
||||
} `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)
|
||||
`And the response should contain`("$.result[0].createdBy.id", "46e0bda9-ca6a-4c65-a58b-7e7267a0bbc5")
|
||||
`And the response should contain`("$.result[0].target.id", "34ddd50a-da00-4a90-a869-08baa2a121be")
|
||||
`And the response should contain list`("$.result[*]", 1, 1)
|
||||
`And the response should contain list`("$.result[*]", 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
package integration
|
||||
|
||||
import integration.steps.then.`And the response should not be null`
|
||||
import integration.steps.then.`Then the response should be`
|
||||
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.then.and
|
||||
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 citizen`
|
||||
import integration.steps.given.`Given I have comment on article`
|
||||
import integration.steps.given.`Given I have comment on comment`
|
||||
import integration.steps.given.`authenticated as`
|
||||
import integration.steps.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.Created
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
import org.junit.jupiter.api.Tag
|
||||
import org.junit.jupiter.api.Tags
|
||||
@@ -30,4 +40,126 @@ class `Comment routes` : BaseTest() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I can create comment`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Hubert", "Reeves")
|
||||
`Given I have comment on comment`(id = "49933147-fc0f-4e5c-aa8d-f77fa0d88fa6")
|
||||
`When I send a POST request`("/comments/49933147-fc0f-4e5c-aa8d-f77fa0d88fa6") {
|
||||
`authenticated as`("Hubert", "Reeves")
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` Created and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.content", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot create comment with bad request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Hubert", "Reeves")
|
||||
`Given I have comment on comment`(id = "49933147-fc0f-4e5c-aa8d-f77fa0d88fa6")
|
||||
`When I send a POST request`("/comments/49933147-fc0f-4e5c-aa8d-f77fa0d88fa6", ALL - REQUEST_BODY) {
|
||||
`authenticated as`("Hubert", "Reeves")
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"content": "small content"
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[0].name", ".content")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I can edit comment`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Hubert", "Reeves")
|
||||
`Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1")
|
||||
`Given I have comment on article`(
|
||||
article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1",
|
||||
createdBy = CitizenI.Name(
|
||||
"Hubert",
|
||||
"Reeves"
|
||||
),
|
||||
id = "fd30d20f-656c-42c6-8955-f61c04537464"
|
||||
)
|
||||
`When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464") {
|
||||
`authenticated as`("Hubert", "Reeves")
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.content", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I cannot edit comment with bad request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Hubert", "Reeves")
|
||||
`Given I have article`(id = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1")
|
||||
`Given I have comment on article`(
|
||||
article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1",
|
||||
createdBy = CitizenI.Name(
|
||||
"Hubert",
|
||||
"Reeves"
|
||||
),
|
||||
id = "fd30d20f-656c-42c6-8955-f61c04537464"
|
||||
)
|
||||
`When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464", ALL - REQUEST_BODY) {
|
||||
`authenticated as`("Hubert", "Reeves")
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"content": "small content"
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[0].name", ".content")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must have at least 20 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I can get comment by its ID`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Alfred", "Kastler")
|
||||
`Given I have article`(id = "3897465b-19d2-43a0-86ea-1e29dbb11ec9")
|
||||
`Given I have comment on article`(
|
||||
article = "3897465b-19d2-43a0-86ea-1e29dbb11ec9",
|
||||
createdBy = CitizenI.Name("Alfred", "Kastler"),
|
||||
id = "edd296a8-fc7a-4717-a2bb-9f035ceca3c2",
|
||||
content = "Hello boy"
|
||||
)
|
||||
`When I send a GET request`("/comments/edd296a8-fc7a-4717-a2bb-9f035ceca3c2") {
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.content", "Hello boy")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package integration
|
||||
|
||||
import integration.steps.`when`.Validate
|
||||
import integration.steps.then.`And have property`
|
||||
import integration.steps.then.`And the response should not be null`
|
||||
import integration.steps.then.`Then the response should be`
|
||||
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.then.`whish contains`
|
||||
import integration.steps.then.and
|
||||
import integration.steps.given.`Given I have citizen`
|
||||
import integration.steps.given.`Given I have constitution`
|
||||
import integration.steps.given.`Given I have constitutions`
|
||||
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 not be null`
|
||||
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.Created
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
@@ -28,12 +31,25 @@ class `Constitution routes` : BaseTest() {
|
||||
fun `I can get constitution list`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have constitutions`(3)
|
||||
`When I send a GET request`("/constitutions") `Then the response should be` OK and {
|
||||
`When I send a GET request`("/constitutions?page=1&limit=10&sort=title&direction=desc") `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot get constitution list with wrong request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have constitutions`(3)
|
||||
`When I send a GET request`("/constitutions?page=1&limit=5000&sort=title&direction=desc", ALL - REQUEST_PARAM) `Then the response should be` BadRequest and {
|
||||
`And the response should not be null`()
|
||||
`And the response should contain`("$.invalidParams[0].name", ".limit")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must be at most '50'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `I can get constitution by ID`() {
|
||||
withIntegrationApplication {
|
||||
@@ -41,7 +57,7 @@ class `Constitution routes` : BaseTest() {
|
||||
`Given I have constitution`("0321c8d1-4ce3-4763-b5f4-a92611d280b4")
|
||||
`When I send a GET request`("/constitutions/0321c8d1-4ce3-4763-b5f4-a92611d280b4") `Then the response should be` OK and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.id") `whish contains` "0321c8d1-4ce3-4763-b5f4-a92611d280b4"
|
||||
`And have property`("$.id") `which contains` "0321c8d1-4ce3-4763-b5f4-a92611d280b4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,33 +82,37 @@ class `Constitution routes` : BaseTest() {
|
||||
`Given I have citizen`("Henri", "Poincaré")
|
||||
`When I send a POST request`("/constitutions") {
|
||||
`authenticated as`("Henri", "Poincaré")
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e",
|
||||
"title":"Hello world!",
|
||||
"title":"Cras sit amet sapien mattis nulla rutrum blandit.",
|
||||
"anonymous":true,
|
||||
"titles":[
|
||||
{
|
||||
"name":"plop"
|
||||
"name":"Cras sit amet sapien mattis nulla rutrum blandit."
|
||||
}
|
||||
]
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` Created and {
|
||||
`And the response should not be null`()
|
||||
`And have property`("$.versionId") `whish contains` "15814bb6-8d90-4c6a-a456-c3939a8ec75e"
|
||||
`And have property`("$.title") `whish contains` "Hello world!"
|
||||
`And have property`("$.versionId") `which contains` "15814bb6-8d90-4c6a-a456-c3939a8ec75e"
|
||||
`And have property`("$.title") `which contains` "Cras sit amet sapien mattis nulla rutrum blandit."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot create an constitution if bad request`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Henri", "Poincaré")
|
||||
`When I send a POST request`("/constitutions", Validate.ALL - Validate.REQUEST_BODY) {
|
||||
`When I send a POST request`("/constitutions", ALL - REQUEST_BODY) {
|
||||
`authenticated as`("Henri", "Poincaré")
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e",
|
||||
"title":"Hello world!",
|
||||
@@ -104,8 +124,39 @@ class `Constitution routes` : BaseTest() {
|
||||
}
|
||||
]
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` BadRequest
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("BadRequest")
|
||||
fun `I cannot create an constitution if request is not valid`() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Henri", "Poincaré")
|
||||
`When I send a POST request`("/constitutions", ALL - REQUEST_BODY) {
|
||||
`authenticated as`("Henri", "Poincaré")
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e",
|
||||
"title":"too small",
|
||||
"anonymous":true,
|
||||
"titles":[
|
||||
{
|
||||
"name":"too small"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` BadRequest and {
|
||||
`And the response should contain`("$.invalidParams[0].name", ".title")
|
||||
`And the response should contain`("$.invalidParams[0].reason", "must have at least 10 characters")
|
||||
`And the response should contain`("$.invalidParams[1].name", ".titles[0].name")
|
||||
`And the response should contain`("$.invalidParams[1].reason", "must have at least 10 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package integration
|
||||
|
||||
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.`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.then.and
|
||||
import integration.steps.given.`And follow article`
|
||||
import integration.steps.given.`Given I have article`
|
||||
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 io.ktor.http.HttpStatusCode.Companion.Created
|
||||
import io.ktor.http.HttpStatusCode.Companion.NoContent
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package integration
|
||||
|
||||
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.`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.then.and
|
||||
import integration.steps.given.`And follow constitution`
|
||||
import integration.steps.given.`Given I have citizen`
|
||||
import integration.steps.given.`Given I have constitution`
|
||||
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 io.ktor.http.HttpStatusCode.Companion.Created
|
||||
import io.ktor.http.HttpStatusCode.Companion.NoContent
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package integration
|
||||
|
||||
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 should contains`
|
||||
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.`authenticated as`
|
||||
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 should contains`
|
||||
import integration.steps.then.and
|
||||
import io.ktor.http.HttpStatusCode.Companion.NoContent
|
||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||
@@ -23,12 +23,14 @@ class `Login routes` : BaseTest() {
|
||||
withIntegrationApplication {
|
||||
`Given I have citizen`("Niels", "Bohr")
|
||||
`When I send a POST request`("/login") {
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"username": "niels-bohr",
|
||||
"password": "azerty"
|
||||
"password": "Azerty123!"
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` OK and {
|
||||
`And the response should not be null`() `and should contains` "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9."
|
||||
// TODO valid requestBody
|
||||
@@ -42,12 +44,14 @@ class `Login routes` : BaseTest() {
|
||||
`Given I have citizen`("Leonhard", "Euler", "fabrice.lecomte.be@gmail.com", id = "c606110c-ff0e-4d09-a79e-74632d7bf7bd")
|
||||
`When I send a POST request`("/auth/passwordless") {
|
||||
`authenticated as`("Leonhard", "Euler")
|
||||
`with body`("""
|
||||
`with body`(
|
||||
"""
|
||||
{
|
||||
"url": "https://dc-project.fr/password/reset",
|
||||
"email": "fabrice.lecomte.be@gmail.com"
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
} `Then the response should be` NoContent
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user