42 Commits

Author SHA1 Message Date
4bb458e8d6 Add developer documentation fo create action 2021-04-09 00:20:58 +02:00
921a545877 Merge pull request #92 from flecomte/sonarq
Sonarcloud
2021-04-04 21:01:27 +02:00
ef942b956e Use sonarcloud 2021-04-04 01:35:02 +02:00
ff74ad7e47 Merge pull request #90 from flecomte/improve-test
Improve tests
2021-04-03 00:39:16 +02:00
2bb90ced03 Refactor 'the response should contain list' 2021-04-03 00:31:24 +02:00
a48cd52652 Add Tags on tests 2021-04-03 00:10:01 +02:00
dd4c2dadab Fix parameters schema validation 2021-04-02 23:47:20 +02:00
c81b63aef2 Merge pull request #89 from flecomte/ArticleViewManager
ArticleViewRepository
2021-04-02 12:39:34 +02:00
cb762a446a Move ArticleViewRepository 2021-04-02 12:29:50 +02:00
db810ab0c6 Rename ArticleViewManager to ArticleViewRepository 2021-04-02 12:29:11 +02:00
01c5b78325 Merge pull request #87 from flecomte/jwt-token-into-env
move JWT secret into ENV
2021-03-31 18:23:13 +02:00
1bc7293660 move JWT secret into ENV 2021-03-31 17:58:47 +02:00
55c890aca5 Merge pull request #86
move "Check auth on all routes" extension into the class
2021-03-31 12:31:51 +02:00
c0e364637a move "Check auth on all routes" extension into the class 2021-03-31 12:30:37 +02:00
0a1ed9ba82 Merge pull request #85 from flecomte/optimise-testsql
Opimize testSql
2021-03-31 03:09:12 +02:00
620085fda8 Optimise gradle task TestSql 2021-03-31 02:58:37 +02:00
3b5c1cf68a Merge pull request #84 from flecomte/69
Error codes
2021-03-31 02:53:58 +02:00
a0d07e88a1 Fix all security routes 2021-03-31 02:43:43 +02:00
f17277c0e9 Test all security of routes #76 2021-03-31 02:35:59 +02:00
9f13213a35 Fix error text when openapi definition was not found 2021-03-27 21:57:53 +01:00
5f0b8de159 #69 Format HTTP error
add 403 for /articles route
2021-03-26 01:53:41 +01:00
6b66130ddc #69 Move HttpStatusPage catch 2021-03-25 23:40:05 +01:00
7f93ec5044 Merge pull request #82 from flecomte/lint
Optimize CI
2021-03-25 02:07:52 +01:00
1be608e6b2 Add Codacy badge 2021-03-25 02:05:05 +01:00
b13cd5544c add coveralls on CI 2021-03-25 01:53:57 +01:00
104f0fb3fc remove distZip & distTar 2021-03-24 23:06:38 +01:00
b2f40ff421 Restrict CI on pull_request on master 2021-03-24 21:56:58 +01:00
09e81620a1 rollback lintCheck after test, create task testAll 2021-03-24 21:32:28 +01:00
7e16c7bb74 Merge pull request #81
Lint
2021-03-24 19:49:34 +01:00
fe953fc967 lintCheck after test 2021-03-24 19:48:14 +01:00
453fd2225c Merge pull request #80
rename openapi file
2021-03-24 19:39:23 +01:00
70fd54d831 Fix destination installation doc 2021-03-24 19:35:11 +01:00
dcf7a2bc06 Rename openapi file 2021-03-24 19:34:39 +01:00
118af0170a Remove unused openapi file 2021-03-24 19:34:00 +01:00
0aa8089a9a Merge pull request #79
Add codefactor badge
2021-03-24 19:29:44 +01:00
fef5f3b396 CodeFactor badge 2021-03-24 19:27:09 +01:00
1838b90ac9 Merge pull request #78
Create CI
2021-03-24 19:12:41 +01:00
73fa2be91f Split CI task test 2021-03-24 19:11:04 +01:00
52183abd08 Lint 2021-03-24 19:08:41 +01:00
e19266d4cc Create CI Action 2021-03-24 19:07:02 +01:00
f458d7b674 Move SQL test files 2021-03-24 19:07:01 +01:00
29d4d6ec25 Merge pull request #77 from flecomte/refactoring-component-and-immutable
Big refactoring
2021-03-24 19:06:06 +01:00
104 changed files with 2422 additions and 3693 deletions

134
.github/workflows/tests.yml vendored Normal file
View 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
View File

@@ -11,7 +11,7 @@
<driver-ref>postgresql</driver-ref> <driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver> <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>
<data-source source="LOCAL" name="sonar@localhost" uuid="ee78beab-120d-4740-ad21-d4d9e2121d25"> <data-source source="LOCAL" name="sonar@localhost" uuid="ee78beab-120d-4740-ad21-d4d9e2121d25">
<driver-ref>postgresql</driver-ref> <driver-ref>postgresql</driver-ref>

View File

@@ -1,6 +1,11 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Sonarqube" type="GradleRunConfiguration" factoryName="Gradle"> <configuration default="false" name="Sonarqube" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="env">
<map>
<entry key="SONAR_TOKEN" value="15ad34f46763706727d884ced12c48d5222fe639" />
</map>
</option>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <option name="externalSystemIdString" value="GRADLE" />

View File

@@ -1,6 +1,16 @@
# DC Project # DC Project
[Installation](./doc/installation) [![CodeFactor](https://www.codefactor.io/repository/github/flecomte/dc-project/badge?s=869dc426625a253a07bea95f9380e23fdb048b94)](https://www.codefactor.io/repository/github/flecomte/dc-project)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/0ec4fe63370148ca956974f90f8d55be)](https://www.codacy.com/gh/flecomte/dc-project/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=flecomte/dc-project&amp;utm_campaign=Badge_Grade)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=dc-project)
[![Tests](https://github.com/flecomte/dc-project/actions/workflows/tests.yml/badge.svg)](https://github.com/flecomte/dc-project/actions/workflows/tests.yml)
[![Coverage Status](https://coveralls.io/repos/github/flecomte/dc-project/badge.svg?branch=master)](https://coveralls.io/github/flecomte/dc-project?branch=master)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=coverage)](https://sonarcloud.io/dashboard?id=dc-project)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=dc-project&metric=ncloc)](https://sonarcloud.io/dashboard?id=dc-project)
[Installation](./doc/installation/Installation.md)
### Run dockers ### Run dockers
```bash ```bash

View File

@@ -41,6 +41,7 @@ plugins {
id("net.nemerosa.versioning") version "2.14.0" id("net.nemerosa.versioning") version "2.14.0"
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1" id("io.gitlab.arturbosch.detekt") version "1.16.0-RC1"
id("com.avast.gradle.docker-compose") version "0.14.0" id("com.avast.gradle.docker-compose") version "0.14.0"
id("com.github.kt3k.coveralls") version "2.8.4"
} }
application { application {
@@ -56,10 +57,13 @@ buildscript {
} }
dependencies { dependencies {
classpath("com.typesafe:config:1.4.1") 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> { tasks.withType<KotlinCompile> {
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
@@ -71,7 +75,7 @@ val migration by tasks.registering {
dependsOn(tasks.named("composeUp")) dependsOn(tasks.named("composeUp"))
doLast { 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( val connection = Connection(
host = config.getString("db.host"), host = config.getString("db.host"),
port = config.getInt("db.port"), port = config.getInt("db.port"),
@@ -81,8 +85,8 @@ val migration by tasks.registering {
) )
Migrations( Migrations(
connection, connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(), file("$buildDir/resources/main/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI() file("$buildDir/resources/main/sql/functions").toURI()
).run { ).run {
run() run()
} }
@@ -94,7 +98,7 @@ val migrationTest by tasks.registering {
dependsOn(tasks.named("testComposeUp")) dependsOn(tasks.named("testComposeUp"))
finalizedBy(tasks.named("testComposeDown")) finalizedBy(tasks.named("testComposeDown"))
doLast { 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( val connection = Connection(
host = config.getString("db.host"), host = config.getString("db.host"),
port = config.getInt("db.port"), port = config.getInt("db.port"),
@@ -104,8 +108,8 @@ val migrationTest by tasks.registering {
) )
Migrations( Migrations(
connection, connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(), file("$buildDir/resources/main/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI() file("$buildDir/resources/main/sql/functions").toURI()
).run { ).run {
run() run()
connection.disconnect() connection.disconnect()
@@ -115,11 +119,13 @@ val migrationTest by tasks.registering {
val testSql by tasks.registering { val testSql by tasks.registering {
group = "verification" group = "verification"
dependsOn(tasks.named("testComposeUp")) dependsOn(tasks.named("processResources"))
finalizedBy(tasks.named("testComposeDown")) dependsOn(tasks.named("processTestResources"))
dependsOn(tasks.named("testSqlComposeUp"))
finalizedBy(tasks.named("testSqlComposeDown"))
doLast { 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( val connection = Connection(
host = config.getString("db.host"), host = config.getString("db.host"),
@@ -131,16 +137,14 @@ val testSql by tasks.registering {
Migrations( Migrations(
connection, connection,
file("$buildDir/../src/main/resources/sql/migrations").toURI(), file("$buildDir/resources/main/sql/migrations").toURI(),
file("$buildDir/../src/main/resources/sql/functions").toURI(), file("$buildDir/resources/main/sql/functions").toURI(),
file("$buildDir/../src/test/sql/fixtures").toURI() file("$buildDir/resources/test/sql/fixtures").toURI()
).run { ).run()
run()
}
Requester.RequesterFactory( Requester.RequesterFactory(
connection = connection, connection = connection,
queriesDirectory = file("$buildDir/../src/test/sql").toURI() queriesDirectory = file("$buildDir/resources/test/sql").toURI()
).createRequester().run { ).createRequester().run {
getQueries().map { getQueries().map {
try { try {
@@ -178,7 +182,11 @@ tasks.named<ShadowJar>("shadowJar") {
archiveFileName.set("${archiveBaseName.get()}-latest-all.${archiveExtension.get()}") 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) { val sourcesJar by tasks.registering(Jar::class) {
group = "build" group = "build"
@@ -194,6 +202,17 @@ tasks.test {
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run 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") apply(plugin = "docker-compose")
dockerCompose { dockerCompose {
projectName = "dc-project" projectName = "dc-project"
@@ -203,23 +222,22 @@ dockerCompose {
removeVolumes = false removeVolumes = false
removeContainers = false removeContainers = false
isRequiredBy(project.tasks.run) 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 { createNested("test").apply {
projectName = "dc-project_test" projectName = "dc-project_test"
useComposeFiles = listOf("docker-compose-test.yml") useComposeFiles = listOf("docker-compose-test.yml")
stopContainers = false stopContainers = false
isRequiredBy(project.tasks.test) 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 { publishing {
if (versioning.info.dirty == false) { if (versioning.info.dirty == false) {
@@ -267,6 +285,7 @@ tasks.jacocoTestReport {
detekt { detekt {
buildUponDefaultConfig = true // preconfigure defaults buildUponDefaultConfig = true // preconfigure defaults
ignoreFailures = true
// config = files("$projectDir/config/detekt.yml") // point to your custom config defining rules to run, overwriting default behavior // 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 // baseline = file("$projectDir/config/baseline.xml") // a way of suppressing issues before introducing detekt
@@ -281,6 +300,7 @@ detekt {
tasks.withType<Detekt> { tasks.withType<Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution. // Target version of the generated JVM bytecode. It is used for type resolution.
this.jvmTarget = "11" this.jvmTarget = "11"
ignoreFailures = true
} }
val setMaxMapCount = tasks.create<Exec>("setMaxMapCount") { val setMaxMapCount = tasks.create<Exec>("setMaxMapCount") {
@@ -293,7 +313,12 @@ val setMaxMapCount = tasks.create<Exec>("setMaxMapCount") {
} }
} }
} }
tasks.named("testComposeUp").configure { dependsOn(setMaxMapCount) }
tasks.named("testComposeUp").configure {
if (OperatingSystem.current().isWindows) {
dependsOn(setMaxMapCount)
}
}
dependencyCheck { dependencyCheck {
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML) formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
@@ -327,7 +352,7 @@ dependencies {
implementation("net.pearx.kasechange:kasechange-jvm:1.3.0") implementation("net.pearx.kasechange:kasechange-jvm:1.3.0")
implementation("com.auth0:java-jwt:3.12.0") implementation("com.auth0:java-jwt:3.12.0")
implementation("com.github.jasync-sql:jasync-postgresql:1.1.6") 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("com.sendgrid:sendgrid-java:4.7.1")
implementation("io.lettuce:lettuce-core:5.3.6.RELEASE") // TODO update to 6.0.2 implementation("io.lettuce:lettuce-core:5.3.6.RELEASE") // TODO update to 6.0.2
implementation("com.rabbitmq:amqp-client:5.10.0") implementation("com.rabbitmq:amqp-client:5.10.0")

30
doc/CreateAction.md Normal file
View File

@@ -0,0 +1,30 @@
Create Action
============
* [ ] Create [OpenApi](../src/main/resources/openapi.yaml) documentation
* [ ] Create route
* [ ] Create request with [Location](https://ktor.io/docs/features-locations.html)
* [ ] Create Validation of request with [Konform](https://www.konform.io)
* [ ] Test validation
* [ ] [Check auth](../src/main/kotlin/fr/dcproject/component/auth/CitizenContext.kt) on protected route
* [ ] [Create test for auth](../src/test/kotlin/integration/steps/given/Auth.kt)
* [ ] Return must not be an Entity
* [ ] Tests request:
* [ ] Route with these params
* [ ] Body of the request
* [ ] Success
* [ ] BadRequest
* [ ] Body and request params must [match with the openapi schema](../src/test/kotlin/integration/steps/then/schema.kt)
* [ ] Create [AccessControl](../src/main/kotlin/fr/dcproject/common/security/AccessControlModule.kt)
* [ ] Test [AccessControl](../src/test/kotlin/integration/steps/given/Auth.kt)
* [ ] Create Entity
* [ ] Create Repository
* [ ] Create SQL function in file
* [ ] Create Tests SQL
* [ ] Tests
* [ ] Test BadRequest

View File

@@ -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:

View File

@@ -38,6 +38,9 @@ services:
REDIS_CONNECTION: ${REDIS_CONNECTION} REDIS_CONNECTION: ${REDIS_CONNECTION}
RABBITMQ_CONNECTION: ${RABBITMQ_CONNECTION} RABBITMQ_CONNECTION: ${RABBITMQ_CONNECTION}
ELASTICSEARCH_CONNECTION: ${ELASTICSEARCH_CONNECTION} ELASTICSEARCH_CONNECTION: ${ELASTICSEARCH_CONNECTION}
JWT_SECRET: ${JWT_SECRET}
JWT_ISSUER: ${JWT_ISSUER}
JWT_VALIDITY: ${JWT_VALIDITY}
depends_on: depends_on:
- elasticsearch - elasticsearch
- db - db

View File

@@ -1,9 +1,7 @@
kotlin.code.style=official kotlin.code.style=official
systemProp.sonar.host.url=http://localhost:9002 systemProp.sonar.host.url=https://sonarcloud.io
systemProp.sonar.login=admin
systemProp.sonar.password=sonar
systemProp.sonar.projectKey=dc-project systemProp.sonar.projectKey=dc-project
systemProp.sonar.projectName=DC Project systemProp.sonar.projectName=DC Project
systemProp.sonar.organization=flecomte
systemProp.sonar.java.coveragePlugin=jacoco systemProp.sonar.java.coveragePlugin=jacoco
systemProp.sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml systemProp.sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
systemProp.sonar.kotlin.detekt.reportPaths=build/reports/detekt/detekt.xml

View File

@@ -6,17 +6,14 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.joda.JodaModule 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.PROD
import fr.dcproject.application.Env.TEST 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.articleKoinModule
import fr.dcproject.component.article.routes.installArticleRoutes import fr.dcproject.component.article.routes.installArticleRoutes
import fr.dcproject.component.auth.ForbiddenException
import fr.dcproject.component.auth.authKoinModule import fr.dcproject.component.auth.authKoinModule
import fr.dcproject.component.auth.jwt.jwtInstallation import fr.dcproject.component.auth.jwt.jwtInstallation
import fr.dcproject.component.auth.routes.installAuthRoutes import fr.dcproject.component.auth.routes.installAuthRoutes
import fr.dcproject.component.auth.user
import fr.dcproject.component.citizen.citizenKoinModule import fr.dcproject.component.citizen.citizenKoinModule
import fr.dcproject.component.citizen.routes.installCitizenRoutes import fr.dcproject.component.citizen.routes.installCitizenRoutes
import fr.dcproject.component.comment.article.routes.installCommentArticleRoutes import fr.dcproject.component.comment.article.routes.installCommentArticleRoutes
@@ -41,7 +38,6 @@ import fr.dcproject.component.workgroup.workgroupKoinModule
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.ApplicationStopped import io.ktor.application.ApplicationStopped
import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
import io.ktor.auth.Authentication import io.ktor.auth.Authentication
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
@@ -51,17 +47,14 @@ import io.ktor.features.CORS
import io.ktor.features.CallLogging import io.ktor.features.CallLogging
import io.ktor.features.ContentNegotiation import io.ktor.features.ContentNegotiation
import io.ktor.features.DataConversion import io.ktor.features.DataConversion
import io.ktor.features.NotFoundException
import io.ktor.features.StatusPages import io.ktor.features.StatusPages
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.cio.websocket.pingPeriod import io.ktor.http.cio.websocket.pingPeriod
import io.ktor.http.cio.websocket.timeout import io.ktor.http.cio.websocket.timeout
import io.ktor.jackson.jackson import io.ktor.jackson.jackson
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations import io.ktor.locations.Locations
import io.ktor.response.respond
import io.ktor.routing.Routing import io.ktor.routing.Routing
import io.ktor.server.jetty.EngineMain import io.ktor.server.jetty.EngineMain
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
@@ -73,7 +66,6 @@ import org.koin.ktor.ext.Koin
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
import org.slf4j.event.Level import org.slf4j.event.Level
import java.time.Duration import java.time.Duration
import java.util.concurrent.CompletionException
fun main(args: Array<String>): Unit = EngineMain.main(args) 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) install(AutoHeadResponse)
@@ -171,26 +163,7 @@ fun Application.module(env: Env = PROD) {
installDocRoutes() installDocRoutes()
} }
install(StatusPages) { install(StatusPages, statusPagesInstallation())
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(CORS) { install(CORS) {
method(HttpMethod.Options) method(HttpMethod.Options)

View File

@@ -43,4 +43,15 @@ class Configuration(val config: Config) {
val rabbitmq: String = config.getString("rabbitmq.connection") val rabbitmq: String = config.getString("rabbitmq.connection")
val exchangeNotificationName = "notification" val exchangeNotificationName = "notification"
val sendGridKey: String = config.getString("mail.sendGrid.key") 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")
}
} }

View File

@@ -9,6 +9,7 @@ import com.fasterxml.jackson.datatype.joda.JodaModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.common.email.Mailer import fr.dcproject.common.email.Mailer
import fr.dcproject.component.auth.jwt.JwtConfig
import fr.dcproject.component.notification.NotificationConsumer import fr.dcproject.component.notification.NotificationConsumer
import fr.dcproject.component.notification.NotificationEmailSender import fr.dcproject.component.notification.NotificationEmailSender
import fr.dcproject.component.notification.NotificationsPush import fr.dcproject.component.notification.NotificationsPush
@@ -25,6 +26,19 @@ import org.koin.dsl.module
@KtorExperimentalAPI @KtorExperimentalAPI
val KoinModule = module { 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 // SQL connection
single { single {
val config: Configuration = get() val config: Configuration = get()

View File

@@ -0,0 +1,82 @@
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.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import java.util.concurrent.CompletionException
class HttpError(
statusCode: HttpStatusCode,
val cause: Throwable? = null,
val type: String? = null,
val title: String = cause?.message ?: statusCode.description,
val detail: String? = null,
val invalidParams: List<InvalidParam>? = null,
val stackTrace: String? = cause?.stackTraceToString()
) {
val statusCode: Int = statusCode.value
data class InvalidParam(
val name: String,
val reason: String
)
}
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)
}
}
}

View 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"

View File

@@ -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.entity.VersionableId
import fr.dcproject.common.utils.contentToString import fr.dcproject.common.utils.contentToString
import fr.dcproject.common.utils.getJsonField import fr.dcproject.common.utils.getJsonField
import fr.dcproject.common.utils.toIso import fr.dcproject.common.utils.toIso
import fr.dcproject.component.article.database.ArticleI
import fr.dcproject.component.citizen.database.CitizenI 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 fr.dcproject.component.views.entity.ViewAggregation
import org.elasticsearch.client.Request import org.elasticsearch.client.Request
import org.elasticsearch.client.Response
import org.elasticsearch.client.RestClient import org.elasticsearch.client.RestClient
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.UUID import java.util.UUID
@@ -17,11 +15,11 @@ import java.util.UUID
/** /**
* Wrapper for manage views with elasticsearch * 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 * 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 isLogged = (citizen != null).toString()
val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!! val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!!
val request = Request( val request = Request(
@@ -45,7 +43,7 @@ class ArticleViewManager <A> (private val restClient: RestClient) : ViewManager<
) )
} }
return restClient.performRequest(request) restClient.performRequest(request)
} }
/** /**
@@ -59,33 +57,33 @@ class ArticleViewManager <A> (private val restClient: RestClient) : ViewManager<
//language=JSON //language=JSON
setJsonEntity( setJsonEntity(
""" """
{ {
"size": 0, "size": 0,
"query": { "query": {
"bool": { "bool": {
"must": { "must": {
"term": { "term": {
"version_id": "${entity.versionId}" "version_id": "${entity.versionId}"
}
}
}
},
"aggs" : {
"total": {
"composite" : {
"sources" : [
{ "version_id": { "terms": {"field": "version_id" } } }
]
}
},
"unique" : {
"cardinality" : {
"field" : "user_ref",
"precision_threshold": 1
}
} }
} }
} }
},
"aggs" : {
"total": {
"composite" : {
"sources" : [
{ "version_id": { "terms": {"field": "version_id" } } }
]
}
},
"unique" : {
"cardinality" : {
"field" : "user_ref",
"precision_threshold": 1
}
}
}
}
""".trimIndent() """.trimIndent()
) )
} }

View File

@@ -2,10 +2,10 @@ package fr.dcproject.component.article.routes
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.article.ArticleAccessControl 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.ArticleForView
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.article.database.ArticleViewRepository
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.NotFoundException import io.ktor.features.NotFoundException
@@ -24,7 +24,7 @@ object GetOneArticle {
val article = ArticleRef(article) 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> { get<ArticleRequest> {
val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found") val article: ArticleForView = repo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
ac.assert { canView(article, citizenOrNull) } ac.assert { canView(article, citizenOrNull) }
@@ -64,7 +64,7 @@ object GetOneArticle {
val total: Int = a.votes.total val total: Int = a.votes.total
val score: Int = a.votes.score val score: Int = a.votes.score
} }
val views: Any = viewManager.getViewsCount(article).let { v -> val views: Any = viewRepository.getViewsCount(article).let { v ->
object { object {
val total = v.total val total = v.total
val unique = v.unique val unique = v.unique
@@ -76,7 +76,7 @@ object GetOneArticle {
) )
launch { launch {
viewManager.addView(call.request.local.remoteHost, article, citizenOrNull) viewRepository.addView(call.request.local.remoteHost, article, citizenOrNull)
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.Input import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.Input
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.notification.ArticleUpdateNotification import fr.dcproject.component.notification.ArticleUpdateNotification
import fr.dcproject.component.notification.Publisher import fr.dcproject.component.notification.Publisher
import fr.dcproject.component.workgroup.database.WorkgroupRef import fr.dcproject.component.workgroup.database.WorkgroupRef
@@ -54,6 +55,7 @@ object UpsertArticle {
} }
post<UpsertArticleRequest> { post<UpsertArticleRequest> {
mustBeAuth()
val article = call.convertRequestToEntity() val article = call.convertRequestToEntity()
ac.assert { canUpsert(article, citizenOrNull) } ac.assert { canUpsert(article, citizenOrNull) }
repo.upsert(article)?.let { a -> repo.upsert(article)?.let { a ->

View File

@@ -26,7 +26,21 @@ val ApplicationCall.citizenOrNull: CitizenEntity?
GlobalContext.get().koin.get<CitizenRepository>().findByUser(it) 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>.citizen get() = context.citizen
val PipelineContext<Unit, ApplicationCall>.citizenOrNull get() = context.citizenOrNull val PipelineContext<Unit, ApplicationCall>.citizenOrNull get() = context.citizenOrNull
val ApplicationCall.user get() = authentication.principal<User>() 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")
}

View File

@@ -2,13 +2,16 @@ package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import fr.dcproject.component.auth.database.UserI import fr.dcproject.component.auth.database.UserI
import org.koin.core.context.GlobalContext
/** /**
* Produce a token for this combination of User and Account * Produce a token for this combination of User and Account
*/ */
fun UserI.makeToken(): String = JWT.create() fun UserI.makeToken(): String = GlobalContext.get().koin.get<JwtConfig>().run {
.withSubject("Authentication") JWT.create()
.withIssuer(JwtConfig.issuer) .withSubject("Authentication")
.withClaim("id", id.toString()) .withIssuer(issuer)
.withExpiresAt(JwtConfig.getExpiration()) .withClaim("id", id.toString())
.sign(JwtConfig.algorithm) .withExpiresAt(getExpiration())
.sign(algorithm)
}

View File

@@ -5,11 +5,11 @@ import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import java.util.Date import java.util.Date
object JwtConfig { class JwtConfig(
private const val secret = "zAP5MBA4B4Ijz0MZaS48" private val secret: String,
const val issuer = "dc-project.fr" val issuer: String,
private const val validityInMs = 3_600_000 * 10 // 10 hours private val validityInMs: Int,
) {
// TODO change to RSA512 // TODO change to RSA512
val algorithm: Algorithm = Algorithm.HMAC512(secret) val algorithm: Algorithm = Algorithm.HMAC512(secret)

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.auth.jwt package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWTVerifier
import fr.dcproject.component.auth.database.User import fr.dcproject.component.auth.database.User
import fr.dcproject.component.auth.database.UserRepository import fr.dcproject.component.auth.database.UserRepository
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
@@ -9,14 +10,14 @@ import io.ktor.http.auth.HttpAuthHeader
import io.ktor.routing.Routing import io.ktor.routing.Routing
import java.util.UUID 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]. * Setup the JWT authentication to be used in [Routing].
* If the token is valid, the corresponding [User] is fetched from the database. * If the token is valid, the corresponding [User] is fetched from the database.
* The [User] can then be accessed in each [ApplicationCall]. * The [User] can then be accessed in each [ApplicationCall].
*/ */
jwt { jwt {
verifier(JwtConfig.verifier) verifier(verifier)
realm = "dc-project.fr" realm = "dc-project.fr"
validate { validate {
it.payload.getClaim("id").asString()?.let { id -> it.payload.getClaim("id").asString()?.let { id ->
@@ -27,7 +28,7 @@ fun jwtInstallation(userRepo: UserRepository): Authentication.Configuration.() -
/* Token in URL */ /* Token in URL */
jwt("url") { jwt("url") {
verifier(JwtConfig.verifier) verifier(verifier)
realm = "dc-project.fr" realm = "dc-project.fr"
authHeader { call -> authHeader { call ->
call.request.queryParameters["token"]?.let { call.request.queryParameters["token"]?.let {

View File

@@ -6,6 +6,7 @@ import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.database.UserRepository import fr.dcproject.component.auth.database.UserRepository
import fr.dcproject.component.auth.database.UserWithPassword import fr.dcproject.component.auth.database.UserWithPassword
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.CitizenAccessControl import fr.dcproject.component.citizen.CitizenAccessControl
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import io.ktor.application.call import io.ktor.application.call
@@ -29,6 +30,7 @@ object ChangeMyPassword {
fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) { fun Route.changeMyPassword(ac: CitizenAccessControl, userRepository: UserRepository) {
put<ChangePasswordCitizenRequest> { put<ChangePasswordCitizenRequest> {
mustBeAuth()
ac.assert { canChangePassword(it.citizen, citizenOrNull) } ac.assert { canChangePassword(it.citizen, citizenOrNull) }
val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>() val content = call.receiveOrBadRequest<ChangePasswordCitizenRequest.Input>()
userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password") userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword)) ?: throw BadRequestException("Bad Password")

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.CitizenAccessControl import fr.dcproject.component.citizen.CitizenAccessControl
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenRepository import fr.dcproject.component.citizen.database.CitizenRepository
@@ -30,6 +31,7 @@ object FindCitizens {
fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) { fun Route.findCitizen(ac: CitizenAccessControl, repo: CitizenRepository) {
get<CitizensRequest> { get<CitizensRequest> {
mustBeAuth()
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
ac.assert { canView(citizens.result, citizenOrNull) } ac.assert { canView(citizens.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.CitizenAccessControl import fr.dcproject.component.citizen.CitizenAccessControl
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -22,6 +23,7 @@ object GetCurrentCitizen {
fun Route.getCurrentCitizen(ac: CitizenAccessControl) { fun Route.getCurrentCitizen(ac: CitizenAccessControl) {
get<CurrentCitizenRequest> { get<CurrentCitizenRequest> {
mustBeAuth()
val currentUser = citizenOrNull val currentUser = citizenOrNull
if (currentUser === null) { if (currentUser === null) {
call.respond(HttpStatusCode.Unauthorized) call.respond(HttpStatusCode.Unauthorized)

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.citizen.routes
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.CitizenAccessControl import fr.dcproject.component.citizen.CitizenAccessControl
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.citizen.database.CitizenRepository import fr.dcproject.component.citizen.database.CitizenRepository
@@ -26,6 +27,7 @@ object GetOneCitizen {
fun Route.getOneCitizen(ac: CitizenAccessControl, citizenRepository: CitizenRepository) { fun Route.getOneCitizen(ac: CitizenAccessControl, citizenRepository: CitizenRepository) {
get<CitizenRequest> { get<CitizenRequest> {
mustBeAuth()
val citizen = citizenRepository.findById(it.citizen.id) ?: throw NotFoundException("Citizen not found ${it.citizen.id}") val citizen = citizenRepository.findById(it.citizen.id) ?: throw NotFoundException("Citizen not found ${it.citizen.id}")
ac.assert { canView(citizen, citizenOrNull) } ac.assert { canView(citizen, citizenOrNull) }

View File

@@ -6,6 +6,7 @@ import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.article.database.CommentArticleRepository import fr.dcproject.component.comment.article.database.CommentArticleRepository
import fr.dcproject.component.comment.article.routes.CreateCommentArticle.PostArticleCommentRequest.Input import fr.dcproject.component.comment.article.routes.CreateCommentArticle.PostArticleCommentRequest.Input
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
@@ -30,6 +31,7 @@ object CreateCommentArticle {
fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) { fun Route.createCommentArticle(repo: CommentArticleRepository, ac: CommentAccessControl) {
post<PostArticleCommentRequest> { post<PostArticleCommentRequest> {
mustBeAuth()
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>().run {
CommentForUpdate( CommentForUpdate(
target = it.article, target = it.article,

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.comment.article.routes
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.comment.article.database.CommentArticleRepository import fr.dcproject.component.comment.article.database.CommentArticleRepository
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
@@ -25,6 +26,7 @@ object GetCitizenArticleComments {
fun Route.getCitizenArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) { fun Route.getCitizenArticleComments(repo: CommentArticleRepository, ac: CommentAccessControl) {
get<CitizenCommentArticleRequest> { get<CitizenCommentArticleRequest> {
mustBeAuth()
repo.findByCitizen(it.citizen).let { comments -> repo.findByCitizen(it.citizen).let { comments ->
ac.assert { canView(comments.result, citizenOrNull) } ac.assert { canView(comments.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -5,6 +5,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.constitution.database.CommentConstitutionRepository import fr.dcproject.component.comment.constitution.database.CommentConstitutionRepository
import fr.dcproject.component.comment.constitution.routes.CreateConstitutionComment.CreateConstitutionCommentRequest.Input import fr.dcproject.component.comment.constitution.routes.CreateConstitutionComment.CreateConstitutionCommentRequest.Input
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
@@ -30,6 +31,7 @@ object CreateConstitutionComment {
fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) { fun Route.createConstitutionComment(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
post<CreateConstitutionCommentRequest> { post<CreateConstitutionCommentRequest> {
mustBeAuth()
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>().run {
CommentForUpdate( CommentForUpdate(
target = it.constitution, target = it.constitution,

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.comment.constitution.routes
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.comment.constitution.database.CommentConstitutionRepository import fr.dcproject.component.comment.constitution.database.CommentConstitutionRepository
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
@@ -25,6 +26,7 @@ object GetCitizenCommentConstitution {
fun Route.getCitizenCommentConstitution(repo: CommentConstitutionRepository, ac: CommentAccessControl) { fun Route.getCitizenCommentConstitution(repo: CommentConstitutionRepository, ac: CommentAccessControl) {
get<GetCitizenCommentConstitutionRequest> { get<GetCitizenCommentConstitutionRequest> {
mustBeAuth()
val comments = repo.findByCitizen(it.citizen) val comments = repo.findByCitizen(it.citizen)
ac.assert { canView(comments.result, citizenOrNull) } ac.assert { canView(comments.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentForUpdate import fr.dcproject.component.comment.generic.database.CommentForUpdate
import fr.dcproject.component.comment.generic.database.CommentRef import fr.dcproject.component.comment.generic.database.CommentRef
@@ -29,6 +30,7 @@ object CreateCommentChildren {
fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) { fun Route.createCommentChildren(repo: CommentRepository, ac: CommentAccessControl) {
post<CreateCommentChildrenRequest> { post<CreateCommentChildrenRequest> {
mustBeAuth()
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = CommentForUpdate( val newComment = CommentForUpdate(
content = call.receiveOrBadRequest<CreateCommentChildrenRequest.Input>().content, content = call.receiveOrBadRequest<CreateCommentChildrenRequest.Input>().content,

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.CommentAccessControl import fr.dcproject.component.comment.generic.CommentAccessControl
import fr.dcproject.component.comment.generic.database.CommentRef import fr.dcproject.component.comment.generic.database.CommentRef
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
@@ -28,6 +29,7 @@ object EditComment {
fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) { fun Route.editComment(repo: CommentRepository, ac: CommentAccessControl) {
put<EditCommentRequest> { put<EditCommentRequest> {
mustBeAuth()
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
ac.assert { canUpdate(comment, citizenOrNull) } ac.assert { canUpdate(comment, citizenOrNull) }

View File

@@ -6,6 +6,7 @@ import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.Citizen import fr.dcproject.component.citizen.database.Citizen
import fr.dcproject.component.citizen.database.CitizenWithUserI import fr.dcproject.component.citizen.database.CitizenWithUserI
import fr.dcproject.component.constitution.ConstitutionAccessControl import fr.dcproject.component.constitution.ConstitutionAccessControl
@@ -68,6 +69,7 @@ object CreateConstitution {
fun Route.createConstitution(repo: ConstitutionRepository, ac: ConstitutionAccessControl) { fun Route.createConstitution(repo: ConstitutionRepository, ac: ConstitutionAccessControl) {
post<PostConstitutionRequest> { post<PostConstitutionRequest> {
mustBeAuth()
getNewConstitution(call.receiveOrBadRequest(), citizen).let { getNewConstitution(call.receiveOrBadRequest(), citizen).let {
ac.assert { canCreate(it, citizenOrNull) } ac.assert { canCreate(it, citizenOrNull) }
val c = repo.upsert(it) ?: error("Unable to create Constitution") val c = repo.upsert(it) ?: error("Unable to create Constitution")

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowForUpdate import fr.dcproject.component.follow.database.FollowForUpdate
@@ -25,6 +26,7 @@ object FollowArticle {
fun Route.followArticle(repo: FollowArticleRepository, ac: FollowAccessControl) { fun Route.followArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
post<ArticleFollowRequest> { post<ArticleFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen) val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.assert { canCreate(follow, citizenOrNull) } ac.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow) repo.follow(follow)

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.article
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
@@ -25,6 +26,7 @@ object GetMyFollowsArticle {
fun Route.getMyFollowsArticle(repo: FollowArticleRepository, ac: FollowAccessControl) { fun Route.getMyFollowsArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
get<CitizenFollowArticleRequest> { get<CitizenFollowArticleRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen) val follows = repo.findByCitizen(it.citizen)
ac.assert { canView(follows.result, citizenOrNull) } ac.assert { canView(follows.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowArticleRepository
import fr.dcproject.component.follow.database.FollowForUpdate import fr.dcproject.component.follow.database.FollowForUpdate
@@ -25,6 +26,7 @@ object UnfollowArticle {
fun Route.unfollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) { fun Route.unfollowArticle(repo: FollowArticleRepository, ac: FollowAccessControl) {
delete<ArticleFollowRequest> { delete<ArticleFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen) val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
ac.assert { canDelete(follow, citizenOrNull) } ac.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow) repo.unfollow(follow)

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository
@@ -25,6 +26,7 @@ object FollowConstitution {
fun Route.followConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) { fun Route.followConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
post<ConstitutionFollowRequest> { post<ConstitutionFollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen) val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.assert { canCreate(follow, citizenOrNull) } ac.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow) repo.follow(follow)

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository
@@ -25,6 +26,7 @@ object GetMyFollowsConstitution {
fun Route.getMyFollowsConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) { fun Route.getMyFollowsConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
get<CitizenFollowConstitutionRequest> { get<CitizenFollowConstitutionRequest> {
mustBeAuth()
val follows = repo.findByCitizen(it.citizen) val follows = repo.findByCitizen(it.citizen)
ac.assert { canView(follows.result, citizenOrNull) } ac.assert { canView(follows.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.follow.routes.constitution
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.FollowAccessControl
import fr.dcproject.component.follow.database.FollowConstitutionRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository
@@ -25,6 +26,7 @@ object UnfollowConstitution {
fun Route.unfollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) { fun Route.unfollowConstitution(repo: FollowConstitutionRepository, ac: FollowAccessControl) {
delete<ConstitutionUnfollowRequest> { delete<ConstitutionUnfollowRequest> {
mustBeAuth()
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen) val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
ac.assert { canDelete(follow, citizenOrNull) } ac.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow) repo.unfollow(follow)

View File

@@ -5,6 +5,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.toUUID import fr.dcproject.common.utils.toUUID
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.opinion.OpinionAccessControl import fr.dcproject.component.opinion.OpinionAccessControl
import fr.dcproject.component.opinion.database.Opinion import fr.dcproject.component.opinion.database.Opinion
@@ -31,6 +32,7 @@ object GetCitizenOpinions {
fun Route.getCitizenOpinions(repo: OpinionArticleRepository, ac: OpinionAccessControl) { fun Route.getCitizenOpinions(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
get<CitizenOpinions> { get<CitizenOpinions> {
mustBeAuth()
val opinionsEntities: List<Opinion<ArticleRef>> = repo.findCitizenOpinionsByTargets(it.citizen, it.id) val opinionsEntities: List<Opinion<ArticleRef>> = repo.findCitizenOpinionsByTargets(it.citizen, it.id)
ac.assert { canView(opinionsEntities, citizenOrNull) } ac.assert { canView(opinionsEntities, citizenOrNull) }

View File

@@ -5,6 +5,7 @@ import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.opinion.OpinionAccessControl import fr.dcproject.component.opinion.OpinionAccessControl
import fr.dcproject.component.opinion.database.Opinion import fr.dcproject.component.opinion.database.Opinion
@@ -37,6 +38,7 @@ object GetMyOpinionsArticle {
fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) { fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
get<CitizenOpinionsArticleRequest> { get<CitizenOpinionsArticleRequest> {
mustBeAuth()
val opinions: Paginated<Opinion<TargetRef>> = repo.findCitizenOpinions(citizen, it.page, it.limit) val opinions: Paginated<Opinion<TargetRef>> = repo.findCitizenOpinions(citizen, it.page, it.limit)
ac.assert { canView(opinions.result, citizenOrNull) } ac.assert { canView(opinions.result, citizenOrNull) }
call.respond( call.respond(

View File

@@ -6,6 +6,7 @@ import fr.dcproject.common.utils.toUUID
import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.opinion.OpinionAccessControl import fr.dcproject.component.opinion.OpinionAccessControl
import fr.dcproject.component.opinion.database.OpinionChoiceRef import fr.dcproject.component.opinion.database.OpinionChoiceRef
import fr.dcproject.component.opinion.database.OpinionForUpdate import fr.dcproject.component.opinion.database.OpinionForUpdate
@@ -34,6 +35,7 @@ object OpinionArticle {
fun Route.setOpinionOnArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) { fun Route.setOpinionOnArticle(repo: OpinionArticleRepository, ac: OpinionAccessControl) {
put<ArticleOpinion> { put<ArticleOpinion> {
mustBeAuth()
call.receiveOrBadRequest<ArticleOpinion.Body>().ids.map { id -> call.receiveOrBadRequest<ArticleOpinion.Body>().ids.map { id ->
OpinionForUpdate( OpinionForUpdate(
choice = OpinionChoiceRef(id), choice = OpinionChoiceRef(id),

View File

@@ -1,8 +1,8 @@
package fr.dcproject.component.views package fr.dcproject.component.views
import fr.dcproject.application.Configuration import fr.dcproject.application.Configuration
import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleViewRepository
import org.apache.http.HttpHost import org.apache.http.HttpHost
import org.elasticsearch.client.RestClient import org.elasticsearch.client.RestClient
import org.koin.dsl.module import org.koin.dsl.module
@@ -17,6 +17,6 @@ val viewKoinModule = module {
).build().apply { ).build().apply {
createEsIndexForViews() createEsIndexForViews()
} }
ArticleViewManager<ArticleForView>(esClient) ArticleViewRepository<ArticleForView>(esClient)
} }
} }

View File

@@ -2,14 +2,13 @@ package fr.dcproject.component.views
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.views.entity.ViewAggregation import fr.dcproject.component.views.entity.ViewAggregation
import org.elasticsearch.client.Response
import org.joda.time.DateTime import org.joda.time.DateTime
interface ViewManager <T> { interface ViewRepository <T> {
/** /**
* Add view to one entity * 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 * Get Views aggregations

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.toUUID import fr.dcproject.common.utils.toUUID
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteRepository import fr.dcproject.component.vote.database.VoteRepository
@@ -26,6 +27,7 @@ object GetCitizenVotes {
fun Route.getCitizenVote(repo: VoteRepository, ac: VoteAccessControl) { fun Route.getCitizenVote(repo: VoteRepository, ac: VoteAccessControl) {
get<CitizenVotesRequest> { get<CitizenVotesRequest> {
mustBeAuth()
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id) val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
if (votes.isNotEmpty()) { if (votes.isNotEmpty()) {
ac.assert { canView(votes, citizenOrNull) } ac.assert { canView(votes, citizenOrNull) }

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.vote.routes
import fr.dcproject.common.response.toOutput import fr.dcproject.common.response.toOutput
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteArticleRepository import fr.dcproject.component.vote.database.VoteArticleRepository
@@ -31,6 +32,7 @@ object GetCitizenVotesOnArticle {
fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) { fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl) {
get<CitizenVoteArticleRequest> { get<CitizenVoteArticleRequest> {
mustBeAuth()
val votes = repo.findByCitizen(it.citizen, it.page, it.limit) val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
ac.assert { canView(votes.result, citizenOrNull) } ac.assert { canView(votes.result, citizenOrNull) }

View File

@@ -6,6 +6,7 @@ import fr.dcproject.component.article.database.ArticleRef
import fr.dcproject.component.article.database.ArticleRepository import fr.dcproject.component.article.database.ArticleRepository
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteArticleRepository import fr.dcproject.component.vote.database.VoteArticleRepository
import fr.dcproject.component.vote.database.VoteForUpdate import fr.dcproject.component.vote.database.VoteForUpdate
@@ -29,6 +30,7 @@ object PutVoteOnArticle {
fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) { fun Route.putVoteOnArticle(repo: VoteArticleRepository, ac: VoteAccessControl, articleRepo: ArticleRepository) {
put<ArticleVoteRequest> { put<ArticleVoteRequest> {
mustBeAuth()
val input = call.receiveOrBadRequest<ArticleVoteRequest.Input>() val input = call.receiveOrBadRequest<ArticleVoteRequest.Input>()
val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found") val article = articleRepo.findById(it.article.id) ?: throw NotFoundException("Article ${it.article.id} not found")
val vote = VoteForUpdate( val vote = VoteForUpdate(

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.comment.generic.database.CommentRepository import fr.dcproject.component.comment.generic.database.CommentRepository
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteCommentRepository import fr.dcproject.component.vote.database.VoteCommentRepository
@@ -26,6 +27,7 @@ object PutVoteOnComment {
fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, ac: VoteAccessControl) { fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, ac: VoteAccessControl) {
put<CommentVoteRequest> { put<CommentVoteRequest> {
mustBeAuth()
val comment = commentRepo.findById(it.comment)!! val comment = commentRepo.findById(it.comment)!!
val content = call.receiveOrBadRequest<CommentVoteRequest.Content>() val content = call.receiveOrBadRequest<CommentVoteRequest.Content>()
val vote = VoteForUpdate( val vote = VoteForUpdate(

View File

@@ -4,6 +4,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.constitution.database.ConstitutionRef
import fr.dcproject.component.constitution.database.ConstitutionRepository import fr.dcproject.component.constitution.database.ConstitutionRepository
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
@@ -30,6 +31,7 @@ object PutVoteOnConstitution {
fun Route.voteConstitution(repo: VoteConstitutionRepository, ac: VoteAccessControl, constitutionRepo: ConstitutionRepository) { fun Route.voteConstitution(repo: VoteConstitutionRepository, ac: VoteAccessControl, constitutionRepo: ConstitutionRepository) {
put<ConstitutionVoteRequest> { put<ConstitutionVoteRequest> {
mustBeAuth()
val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}") val constitution = constitutionRepo.findById(it.constitution.id) ?: throw NotFoundException("Unable to find constitution ${it.constitution.id}")
val content = call.receiveOrBadRequest<Input>() val content = call.receiveOrBadRequest<Input>()
val vote = VoteForUpdate( val vote = VoteForUpdate(

View File

@@ -5,6 +5,7 @@ import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupForUpdate import fr.dcproject.component.workgroup.database.WorkgroupForUpdate
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
@@ -33,6 +34,7 @@ object CreateWorkgroup {
fun Route.createWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.createWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
post<PostWorkgroupRequest> { post<PostWorkgroupRequest> {
mustBeAuth()
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>().run {
WorkgroupForUpdate( WorkgroupForUpdate(
id ?: UUID.randomUUID(), id ?: UUID.randomUUID(),

View File

@@ -2,6 +2,7 @@ package fr.dcproject.component.workgroup.routes
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
import io.ktor.application.call import io.ktor.application.call
@@ -20,6 +21,7 @@ object DeleteWorkgroup {
fun Route.deleteWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.deleteWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
delete<DeleteWorkgroupRequest> { delete<DeleteWorkgroupRequest> {
mustBeAuth()
repo.findById(it.workgroupId)?.let { workgroup -> repo.findById(it.workgroupId)?.let { workgroup ->
ac.assert { canDelete(workgroup, citizenOrNull) } ac.assert { canDelete(workgroup, citizenOrNull) }
repo.delete(workgroup) repo.delete(workgroup)

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupForUpdate import fr.dcproject.component.workgroup.database.WorkgroupForUpdate
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
@@ -31,6 +32,7 @@ object EditWorkgroup {
fun Route.editWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.editWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
put<PutWorkgroupRequest> { put<PutWorkgroupRequest> {
mustBeAuth()
repo.findById(it.workgroupId)?.let { old -> repo.findById(it.workgroupId)?.let { old ->
call.receiveOrBadRequest<Input>().run { call.receiveOrBadRequest<Input>().run {
WorkgroupForUpdate( WorkgroupForUpdate(

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
@@ -44,6 +45,7 @@ object AddMemberToWorkgroup {
fun Route.addMemberToWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.addMemberToWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
/* Add members to workgroup */ /* Add members to workgroup */
post<WorkgroupsMembersRequest> { post<WorkgroupsMembersRequest> {
mustBeAuth()
repo.findById(it.workgroupId)?.let { workgroup -> repo.findById(it.workgroupId)?.let { workgroup ->
call.getMembersFromRequest().let { members -> call.getMembersFromRequest().let { members ->
ac.assert { canAddMembers(workgroup, citizenOrNull) } ac.assert { canAddMembers(workgroup, citizenOrNull) }

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
@@ -35,6 +36,7 @@ object DeleteMembersOfWorkgroup {
fun Route.deleteMemberOfWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.deleteMemberOfWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
/* Delete members of workgroup */ /* Delete members of workgroup */
delete<WorkgroupsMembersRequest> { delete<WorkgroupsMembersRequest> {
mustBeAuth()
repo.findById(it.workgroupId)?.let { workgroup -> repo.findById(it.workgroupId)?.let { workgroup ->
call.getMembersFromRequest() call.getMembersFromRequest()
.let { members -> .let { members ->

View File

@@ -3,6 +3,7 @@ package fr.dcproject.component.workgroup.routes.members
import fr.dcproject.common.security.assert import fr.dcproject.common.security.assert
import fr.dcproject.common.utils.receiveOrBadRequest import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.auth.mustBeAuth
import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRef
import fr.dcproject.component.workgroup.WorkgroupAccessControl import fr.dcproject.component.workgroup.WorkgroupAccessControl
import fr.dcproject.component.workgroup.database.WorkgroupRepository import fr.dcproject.component.workgroup.database.WorkgroupRepository
@@ -42,6 +43,7 @@ object UpdateMemberOfWorkgroup {
fun Route.updateMemberOfWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) { fun Route.updateMemberOfWorkgroup(repo: WorkgroupRepository, ac: WorkgroupAccessControl) {
/* Update members of workgroup */ /* Update members of workgroup */
put<WorkgroupsMembersRequest> { put<WorkgroupsMembersRequest> {
mustBeAuth()
repo.findById(it.workgroupId)?.let { workgroup -> repo.findById(it.workgroupId)?.let { workgroup ->
call.getMembersFromRequest().let { members -> call.getMembersFromRequest().let { members ->
ac.assert { canUpdateMembers(workgroup, citizenOrNull) } ac.assert { canUpdateMembers(workgroup, citizenOrNull) }

View File

@@ -42,3 +42,11 @@ mail {
key = ${?SEND_GRID_KEY} 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

View 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")
}

View File

@@ -22,7 +22,7 @@ import org.koin.test.get
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI @KtorExperimentalAPI
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("functional")) @Tags(Tag("functional"), Tag("mail"))
class MailerTest : KoinTest, AutoCloseKoinTest() { class MailerTest : KoinTest, AutoCloseKoinTest() {
@InternalCoroutinesApi @InternalCoroutinesApi
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi

View File

@@ -33,7 +33,7 @@ import org.junit.jupiter.api.TestInstance
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@TestInstance(TestInstance.Lifecycle.PER_METHOD) @TestInstance(TestInstance.Lifecycle.PER_METHOD)
@Tags(Tag("functional")) @Tags(Tag("functional"), Tag("notification"))
class NotificationConsumerTest { class NotificationConsumerTest {
companion object { companion object {
@BeforeAll @BeforeAll

View File

@@ -24,7 +24,7 @@ import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@Tags(Tag("functional")) @Tags(Tag("functional"), Tag("notification"))
internal class NotificationsPushTest { internal class NotificationsPushTest {
companion object { companion object {
@BeforeAll @BeforeAll

View File

@@ -8,7 +8,7 @@ import org.junit.jupiter.api.TestInstance
import kotlin.test.assertEquals import kotlin.test.assertEquals
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("functional")) @Tags(Tag("functional"), Tag("utils"))
class ResourcesKtTest { class ResourcesKtTest {
@Test @Test
fun readResource() { fun readResource() {

View File

@@ -2,8 +2,8 @@ package functional
import fr.dcproject.application.Env.TEST import fr.dcproject.application.Env.TEST
import fr.dcproject.application.module import fr.dcproject.application.module
import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleForView
import fr.dcproject.component.article.database.ArticleViewRepository
import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserCreator
import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
@@ -25,7 +25,7 @@ import java.util.UUID
@KtorExperimentalAPI @KtorExperimentalAPI
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@TestInstance(PER_CLASS) @TestInstance(PER_CLASS)
@Tags(Tag("functional")) @Tags(Tag("functional"), Tag("view"))
class ViewTest { class ViewTest {
@Test @Test
fun `test View Article`() { fun `test View Article`() {
@@ -44,33 +44,33 @@ class ViewTest {
val citizenRef = CitizenRef() val citizenRef = CitizenRef()
withTestApplication({ module(TEST) }) { withTestApplication({ module(TEST) }) {
val viewManager: ArticleViewManager<ArticleForView> = application.get() val viewRepository: ArticleViewRepository<ArticleForView> = application.get()
/* Get view before */ /* Get view before */
val startView = viewManager.getViewsCount(article) val startView = viewRepository.getViewsCount(article)
/* Add View */ /* Add View */
viewManager.addView( viewRepository.addView(
"1.2.3.4", "1.2.3.4",
article, article,
citizenRef citizenRef
) )
/* Add View */ /* Add View */
viewManager.addView( viewRepository.addView(
"10.10.10.10", "10.10.10.10",
article, article,
citizenRef citizenRef
) )
/* Add View */ /* Add View */
viewManager.addView( viewRepository.addView(
"8.8.8.8", "8.8.8.8",
article article
) )
/* Add View */ /* Add View */
viewManager.addView( viewRepository.addView(
"1.1.1.1", "1.1.1.1",
article article
) )
@@ -79,7 +79,7 @@ class ViewTest {
Thread.sleep(1000) Thread.sleep(1000)
/* Get view */ /* Get view */
val afterView = viewManager.getViewsCount(article) val afterView = viewRepository.getViewsCount(article)
/* Check if view has increment */ /* Check if view has increment */
afterView.total `should be equal to` startView.total + 4 afterView.total `should be equal to` startView.total + 4

View File

@@ -1,22 +1,24 @@
package integration 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 integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.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 created by workgroup`
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have articles` import integration.steps.given.`Given I have articles`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have workgroup` import integration.steps.given.`Given I have workgroup`
import integration.steps.given.`authenticated as` 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 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.`And the response should not contain`
import integration.steps.then.`Then the response should be`
import integration.steps.then.`whish contains`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.Forbidden
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Tags
@@ -36,7 +38,7 @@ class `Article routes` : BaseTest() {
`And the response should contain pattern`("$.result[1].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 contain pattern`("$.result[2].createdBy.name.firstName", "firstName.+")
`And the response should not contain`("$.result[3]") `And the response should not contain`("$.result[3]")
`And the response should contain list`("$.result", 3, 3) `And the response should contain list`("$.result", 3)
} }
} }
} }
@@ -84,22 +86,50 @@ class `Article routes` : BaseTest() {
`Given I have citizen`("John", "Doe") `Given I have citizen`("John", "Doe")
`When I send a POST request`("/articles") { `When I send a POST request`("/articles") {
`authenticated as`("John", "Doe") `authenticated as`("John", "Doe")
`with body`(""" `with body`(
{ """
"versionId": "09c418b6-63ba-448b-b38b-502b41cd500e", {
"title": "title2", "versionId": "09c418b6-63ba-448b-b38b-502b41cd500e",
"anonymous": false, "title": "title2",
"content": "content2", "anonymous": false,
"description": "description2", "content": "content2",
"tags": [ "description": "description2",
"green" "tags": [
] "green"
} ]
""") }
"""
)
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And have property`("$.versionId") `whish contains` "09c418b6-63ba-448b-b38b-502b41cd500e" `And have property`("$.versionId") `whish contains` "09c418b6-63ba-448b-b38b-502b41cd500e"
} }
} }
} }
@Test
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": "content2",
"description": "description2",
"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")
}
}
}
} }

View 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")
}
}
}

View File

@@ -1,16 +1,16 @@
package integration package integration
import integration.steps.`when`.Validate 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 GET request`
import integration.steps.`when`.`When I send a PUT request` import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
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 citizen`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And have property`
import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be`
import integration.steps.then.`whish contains`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
@@ -66,12 +66,14 @@ class `Citizen routes` : BaseTest() {
`Given I have citizen`("Georges", "Charpak", id = "0c966522-4071-43e5-a3ca-cfff2557f2cf") `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") { `When I send a PUT request`("/citizens/0c966522-4071-43e5-a3ca-cfff2557f2cf/password/change") {
`authenticated as`("Georges", "Charpak") `authenticated as`("Georges", "Charpak")
`with body`(""" `with body`(
{ """
"oldPassword": "azerty", {
"newPassword": "qwerty" "oldPassword": "azerty",
} "newPassword": "qwerty"
""") }
"""
)
} `Then the response should be` Created } `Then the response should be` Created
} }
} }
@@ -82,12 +84,14 @@ class `Citizen routes` : BaseTest() {
`Given I have citizen`("Louis", "Breguet", id = "6cf2a19d-d15d-4ee5-b2a9-907afd26b525") `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) { `When I send a PUT request`("/citizens/6cf2a19d-d15d-4ee5-b2a9-907afd26b525/password/change", Validate.ALL - Validate.REQUEST_BODY) {
`authenticated as`("Louis", "Breguet") `authenticated as`("Louis", "Breguet")
`with body`(""" `with body`(
{ """
"plup": "azerty", {
"gloup": "qwerty" "plup": "azerty",
} "gloup": "qwerty"
""") }
"""
)
} `Then the response should be` BadRequest } `Then the response should be` BadRequest
} }
} }

View File

@@ -1,18 +1,18 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.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 GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`When I send a PUT request` import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` 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 article`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have comment on article` import integration.steps.given.`Given I have comment on article`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -30,11 +30,13 @@ class `Comment articles routes` : BaseTest() {
`Given I have article`(id = "aa16c635-28da-46f0-9a89-934eef88c7ca") `Given I have article`(id = "aa16c635-28da-46f0-9a89-934eef88c7ca")
`When I send a POST request`("/articles/aa16c635-28da-46f0-9a89-934eef88c7ca/comments") { `When I send a POST request`("/articles/aa16c635-28da-46f0-9a89-934eef88c7ca/comments") {
`authenticated as`("Michael", "Faraday") `authenticated as`("Michael", "Faraday")
`with body`(""" `with body`(
{ """
"content": "Hello mister" {
} "content": "Hello mister"
""") }
"""
)
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.target.id", "aa16c635-28da-46f0-9a89-934eef88c7ca") `And the response should contain`("$.target.id", "aa16c635-28da-46f0-9a89-934eef88c7ca")
@@ -82,6 +84,7 @@ class `Comment articles routes` : BaseTest() {
`Given I have article`(id = "17df7fb9-b388-4e20-ab19-29c29972da01", createdBy = Name("Erwin", "Schrodinger")) `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")) `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") { `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 { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.currentPage", 1) `And the response should contain`("$.currentPage", 1)
@@ -99,11 +102,13 @@ class `Comment articles routes` : BaseTest() {
`Given I have comment on article`(article = "bb05e4a3-55a1-4088-85e7-8d8c23be29b1", createdBy = Name("Hubert", "Reeves"), id = "fd30d20f-656c-42c6-8955-f61c04537464") `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") { `When I send a PUT request`("/comments/fd30d20f-656c-42c6-8955-f61c04537464") {
`authenticated as`("Hubert", "Reeves") `authenticated as`("Hubert", "Reeves")
`with body`(""" `with body`(
{ """
"content": "Hello boy" {
} "content": "Hello boy"
""") }
"""
)
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.content", "Hello boy") `And the response should contain`("$.content", "Hello boy")

View File

@@ -1,18 +1,18 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.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`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.then.and
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have comment on constitution` import integration.steps.given.`Given I have comment on constitution`
import integration.steps.given.`Given I have constitution` import integration.steps.given.`Given I have constitution`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain`
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.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -30,11 +30,13 @@ class `Comment constitutions routes` : BaseTest() {
`Given I have constitution`(id = "1707c287-a472-4a62-89f2-9e85030e915c") `Given I have constitution`(id = "1707c287-a472-4a62-89f2-9e85030e915c")
`When I send a POST request`("/constitutions/1707c287-a472-4a62-89f2-9e85030e915c/comments") { `When I send a POST request`("/constitutions/1707c287-a472-4a62-89f2-9e85030e915c/comments") {
`authenticated as`("Nicolas", "Copernic") `authenticated as`("Nicolas", "Copernic")
`with body`(""" `with body`(
{ """
"content": "Hello mister" {
} "content": "Hello mister"
""") }
"""
)
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should not be null`() `And the response should not be null`()
} }
@@ -48,13 +50,14 @@ class `Comment constitutions routes` : BaseTest() {
`Given I have constitution`(id = "34ddd50a-da00-4a90-a869-08baa2a121be", createdBy = Name("Charles", "Darwin")) `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")) `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") { `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 { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain`("$.currentPage", 1) `And the response should contain`("$.currentPage", 1)
`And the response should contain`("$.limit", 50) `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].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`("$.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)
} }
} }
} }

View File

@@ -1,13 +1,13 @@
package integration 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.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.then.and
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have comment on article` import integration.steps.given.`Given I have comment on article`
import integration.steps.given.`authenticated as` 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
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Tags

View File

@@ -1,18 +1,18 @@
package integration package integration
import integration.steps.`when`.Validate 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 GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.then.`whish contains`
import integration.steps.then.and
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have constitution` import integration.steps.given.`Given I have constitution`
import integration.steps.given.`Given I have constitutions` import integration.steps.given.`Given I have constitutions`
import integration.steps.given.`authenticated as` 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.`whish contains`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.BadRequest import io.ktor.http.HttpStatusCode.Companion.BadRequest
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
@@ -66,18 +66,20 @@ class `Constitution routes` : BaseTest() {
`Given I have citizen`("Henri", "Poincaré") `Given I have citizen`("Henri", "Poincaré")
`When I send a POST request`("/constitutions") { `When I send a POST request`("/constitutions") {
`authenticated as`("Henri", "Poincaré") `authenticated as`("Henri", "Poincaré")
`with body`(""" `with body`(
{ """
"versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e", {
"title":"Hello world!", "versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e",
"anonymous":true, "title":"Hello world!",
"titles":[ "anonymous":true,
{ "titles":[
"name":"plop" {
} "name":"plop"
] }
} ]
""") }
"""
)
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should not be null`() `And the response should not be null`()
`And have property`("$.versionId") `whish contains` "15814bb6-8d90-4c6a-a456-c3939a8ec75e" `And have property`("$.versionId") `whish contains` "15814bb6-8d90-4c6a-a456-c3939a8ec75e"
@@ -92,19 +94,21 @@ class `Constitution routes` : BaseTest() {
`Given I have citizen`("Henri", "Poincaré") `Given I have citizen`("Henri", "Poincaré")
`When I send a POST request`("/constitutions", Validate.ALL - Validate.REQUEST_BODY) { `When I send a POST request`("/constitutions", Validate.ALL - Validate.REQUEST_BODY) {
`authenticated as`("Henri", "Poincaré") `authenticated as`("Henri", "Poincaré")
`with body`(""" `with body`(
{ """
"versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e", {
"title":"Hello world!", "versionId":"15814bb6-8d90-4c6a-a456-c3939a8ec75e",
"anonymous":true, "title":"Hello world!",
"titles":[ "anonymous":true,
{ "titles":[
"name":"plop", {
"wrongField":0 "name":"plop",
} "wrongField":0
] }
} ]
""") }
"""
)
} `Then the response should be` BadRequest } `Then the response should be` BadRequest
} }
} }

View File

@@ -1,18 +1,18 @@
package integration 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 DELETE request`
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.then.and
import integration.steps.given.`And follow article` import integration.steps.given.`And follow article`
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.given.`with no content` import integration.steps.given.`with no content`
import integration.steps.then.`And 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.Created
import io.ktor.http.HttpStatusCode.Companion.NoContent import io.ktor.http.HttpStatusCode.Companion.NoContent
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,18 +1,18 @@
package integration 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 DELETE request`
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.then.and
import integration.steps.given.`And follow constitution` import integration.steps.given.`And follow constitution`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have constitution` import integration.steps.given.`Given I have constitution`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.given.`with no content` import integration.steps.given.`with no content`
import integration.steps.then.`And 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.Created
import io.ktor.http.HttpStatusCode.Companion.NoContent import io.ktor.http.HttpStatusCode.Companion.NoContent
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK

View File

@@ -1,12 +1,12 @@
package integration 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`.`When I send a POST request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should not be null`
import integration.steps.then.`Then the response should be`
import integration.steps.then.`and should contains`
import integration.steps.then.and import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.NoContent import io.ktor.http.HttpStatusCode.Companion.NoContent
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
@@ -23,15 +23,17 @@ class `Login routes` : BaseTest() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Niels", "Bohr") `Given I have citizen`("Niels", "Bohr")
`When I send a POST request`("/login") { `When I send a POST request`("/login") {
`with body`(""" `with body`(
{ """
"username": "niels-bohr", {
"password": "azerty" "username": "niels-bohr",
} "password": "azerty"
""") }
"""
)
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `and should contains` "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9." `And the response should not be null`() `and should contains` "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9."
//TODO valid requestBody // 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") `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") { `When I send a POST request`("/auth/passwordless") {
`authenticated as`("Leonhard", "Euler") `authenticated as`("Leonhard", "Euler")
`with body`(""" `with body`(
{ """
"url": "https://dc-project.fr/password/reset", {
"email": "fabrice.lecomte.be@gmail.com" "url": "https://dc-project.fr/password/reset",
} "email": "fabrice.lecomte.be@gmail.com"
""") }
"""
)
} `Then the response should be` NoContent } `Then the response should be` NoContent
} }
} }

View File

@@ -1,18 +1,18 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`Then the response should be`
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a PUT request` import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.then.and
import integration.steps.given.`Given I have an opinion choice` import integration.steps.given.`Given I have an opinion choice`
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have opinion on article` import integration.steps.given.`Given I have opinion on article`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`Then the response should be`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("integration"), Tag("article"), Tag("opinion")) @Tags(Tag("integration"), Tag("opinion"))
class `Opinion routes` : BaseTest() { class `Opinion routes` : BaseTest() {
@Test @Test
fun `I can get all opinion choices`() { fun `I can get all opinion choices`() {
@@ -48,6 +48,7 @@ class `Opinion routes` : BaseTest() {
} }
@Test @Test
@Tag("article")
fun `I can create opinion on article`() { fun `I can create opinion on article`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Isaac", "Newton", id = "2f414045-95d9-42ca-a3a9-8cdde52ad253") `Given I have citizen`("Isaac", "Newton", id = "2f414045-95d9-42ca-a3a9-8cdde52ad253")
@@ -55,13 +56,15 @@ class `Opinion routes` : BaseTest() {
`Given I have article`(id = "9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b", createdBy = Name("Isaac", "Newton")) `Given I have article`(id = "9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b", createdBy = Name("Isaac", "Newton"))
`When I send a PUT request`("/articles/9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b/opinions") { `When I send a PUT request`("/articles/9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b/opinions") {
`authenticated as`("Isaac", "Newton") `authenticated as`("Isaac", "Newton")
`with body`(""" `with body`(
{ """
"ids": [ {
"0f4f1721-3136-44f1-9f31-1459f3317b15" "ids": [
] "0f4f1721-3136-44f1-9f31-1459f3317b15"
} ]
""") }
"""
)
} `Then the response should be` Created } `Then the response should be` Created
} }
} }
@@ -87,6 +90,7 @@ class `Opinion routes` : BaseTest() {
} }
@Test @Test
@Tag("article")
fun `I can receive opinion aggregation with article`() { fun `I can receive opinion aggregation with article`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have an opinion choice`("Opinion6") `Given I have an opinion choice`("Opinion6")
@@ -118,6 +122,7 @@ class `Opinion routes` : BaseTest() {
} }
@Test @Test
@Tag("article")
fun `I can get all my opinion of one article`() { fun `I can get all my opinion of one article`() {
withIntegrationApplication { withIntegrationApplication {
`Given I have citizen`("Albert", "Einstein", id = "c1542096-3431-432d-8e35-9dc071d4c818") `Given I have citizen`("Albert", "Einstein", id = "c1542096-3431-432d-8e35-9dc071d4c818")
@@ -132,7 +137,7 @@ class `Opinion routes` : BaseTest() {
`authenticated as`("Albert", "Einstein") `authenticated as`("Albert", "Einstein")
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain`("$.result[0].name", "Opinion9") `And the response should contain`("$.result[0].name", "Opinion9")
`And the response should contain list`("$.result[*]", 1, 1) `And the response should contain list`("$.result[*]", 1)
} }
} }
} }

View File

@@ -22,17 +22,19 @@ class `Register routes` : BaseTest() {
fun `I can register`() { fun `I can register`() {
withIntegrationApplication { withIntegrationApplication {
`When I send a POST request`("/register") { `When I send a POST request`("/register") {
`with body`(""" `with body`(
{ """
"name": {"firstName":"George", "lastName":"MICHEL"}, {
"birthday": "2001-01-01", "name": {"firstName":"George", "lastName":"MICHEL"},
"user":{ "birthday": "2001-01-01",
"username": "george-junior", "user":{
"password": "azerty" "username": "george-junior",
}, "password": "azerty"
"email": "george-junior@gmail.com" },
} "email": "george-junior@gmail.com"
""") }
"""
)
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should not be null`() `And the response should not be null`()
`And the response should contain pattern`("$.token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.") `And the response should contain pattern`("$.token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.")
@@ -44,16 +46,18 @@ class `Register routes` : BaseTest() {
fun `I cannot register if no username was sent`() { fun `I cannot register if no username was sent`() {
withIntegrationApplication { withIntegrationApplication {
`When I send a POST request`("/register", Validate.ALL - Validate.REQUEST_BODY) { `When I send a POST request`("/register", Validate.ALL - Validate.REQUEST_BODY) {
`with body`(""" `with body`(
{ """
"name": {"firstName":"George2", "lastName":"MICHEL2"}, {
"birthday": "2001-01-01", "name": {"firstName":"George2", "lastName":"MICHEL2"},
"user":{ "birthday": "2001-01-01",
"password": "" "user":{
}, "password": ""
"email": "george-junior@gmail.com" },
} "email": "george-junior@gmail.com"
""") }
"""
)
} `Then the response should be` BadRequest and { } `Then the response should be` BadRequest and {
`And the response should be null`() `And the response should be null`()
} }

View File

@@ -1,12 +1,9 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.then.`And the response should contain`
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 GET request`
import integration.steps.`when`.`When I send a PUT request` import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.then.and
import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have article`
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have comment on article` import integration.steps.given.`Given I have comment on article`
@@ -14,6 +11,9 @@ import integration.steps.given.`Given I have constitution`
import integration.steps.given.`Given I have vote +1 on article` import integration.steps.given.`Given I have vote +1 on article`
import integration.steps.given.`Given I have vote -1 on article` import integration.steps.given.`Given I have vote -1 on article`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`Then the response should be`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -31,11 +31,13 @@ class `Vote routes` : BaseTest() {
`Given I have article`(id = "835c5101-ca39-4038-a4e6-da6ee62ca6d5") `Given I have article`(id = "835c5101-ca39-4038-a4e6-da6ee62ca6d5")
`When I send a PUT request`("/articles/835c5101-ca39-4038-a4e6-da6ee62ca6d5/vote") { `When I send a PUT request`("/articles/835c5101-ca39-4038-a4e6-da6ee62ca6d5/vote") {
`authenticated as`("Thalès", "Milet") `authenticated as`("Thalès", "Milet")
`with body`(""" `with body`(
{ """
"note": 1 {
} "note": 1
""") }
"""
)
} `Then the response should be` Created } `Then the response should be` Created
} }
} }
@@ -47,11 +49,13 @@ class `Vote routes` : BaseTest() {
`Given I have constitution`(id = "76e79c89-efc1-492d-9e8f-dc9717363a11") `Given I have constitution`(id = "76e79c89-efc1-492d-9e8f-dc9717363a11")
`When I send a PUT request`("/constitutions/76e79c89-efc1-492d-9e8f-dc9717363a11/vote") { `When I send a PUT request`("/constitutions/76e79c89-efc1-492d-9e8f-dc9717363a11/vote") {
`authenticated as`("Gregor", "Mendel") `authenticated as`("Gregor", "Mendel")
`with body`(""" `with body`(
{ """
"note": 1 {
} "note": 1
""") }
"""
)
} `Then the response should be` Created } `Then the response should be` Created
} }
} }
@@ -102,11 +106,13 @@ class `Vote routes` : BaseTest() {
) )
`When I send a PUT request`("/comments/e793eccc-456b-4450-a292-46d592229b74/vote") { `When I send a PUT request`("/comments/e793eccc-456b-4450-a292-46d592229b74/vote") {
`authenticated as`("Antoine", "Lavoisier") `authenticated as`("Antoine", "Lavoisier")
`with body`(""" `with body`(
{ """
"note": -1 {
} "note": -1
""") }
"""
)
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should contain`("$.down", 1) `And the response should contain`("$.down", 1)
} }

View File

@@ -1,22 +1,22 @@
package integration package integration
import fr.dcproject.component.citizen.database.CitizenI.Name import fr.dcproject.component.citizen.database.CitizenI.Name
import integration.steps.then.`And the response should be null`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain`
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 DELETE request`
import integration.steps.`when`.`When I send a GET request` import integration.steps.`when`.`When I send a GET request`
import integration.steps.`when`.`When I send a POST request` import integration.steps.`when`.`When I send a POST request`
import integration.steps.`when`.`When I send a PUT request` import integration.steps.`when`.`When I send a PUT request`
import integration.steps.`when`.`with body` import integration.steps.`when`.`with body`
import integration.steps.then.and
import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have citizen`
import integration.steps.given.`Given I have workgroup` import integration.steps.given.`Given I have workgroup`
import integration.steps.given.`With members` import integration.steps.given.`With members`
import integration.steps.given.`authenticated as` import integration.steps.given.`authenticated as`
import integration.steps.given.`with no content` import integration.steps.given.`with no content`
import integration.steps.then.`And have property` import integration.steps.then.`And have property`
import integration.steps.then.`And the response should be null`
import integration.steps.then.`And the response should contain list`
import integration.steps.then.`And the response should contain`
import integration.steps.then.`Then the response should be`
import integration.steps.then.and
import io.ktor.http.HttpStatusCode.Companion.Created import io.ktor.http.HttpStatusCode.Companion.Created
import io.ktor.http.HttpStatusCode.Companion.NoContent import io.ktor.http.HttpStatusCode.Companion.NoContent
import io.ktor.http.HttpStatusCode.Companion.NotFound import io.ktor.http.HttpStatusCode.Companion.NotFound
@@ -68,14 +68,16 @@ class `Workgroup routes` : BaseTest() {
`Given I have citizen`("Werner", "Heisenberg") `Given I have citizen`("Werner", "Heisenberg")
`When I send a POST request`("/workgroups") { `When I send a POST request`("/workgroups") {
`authenticated as`("Werner", "Heisenberg") `authenticated as`("Werner", "Heisenberg")
`with body`(""" `with body`(
{ """
"id":"f496d86d-6654-4068-91ff-90e1dbcc5f38", {
"name":"Les Bouffons", "id":"f496d86d-6654-4068-91ff-90e1dbcc5f38",
"description":"La vie est belle", "name":"Les Bouffons",
"anonymous":false "description":"La vie est belle",
} "anonymous":false
""") }
"""
)
} `Then the response should be` Created and { } `Then the response should be` Created and {
`And the response should contain`("$.id", "f496d86d-6654-4068-91ff-90e1dbcc5f38") `And the response should contain`("$.id", "f496d86d-6654-4068-91ff-90e1dbcc5f38")
`And the response should contain`("$.name", "Les Bouffons") `And the response should contain`("$.name", "Les Bouffons")
@@ -103,19 +105,21 @@ class `Workgroup routes` : BaseTest() {
} }
`When I send a PUT request`("/workgroups/aa875a24-0050-4252-9130-d37391714e26") { `When I send a PUT request`("/workgroups/aa875a24-0050-4252-9130-d37391714e26") {
`authenticated as`("John", "Wheeler") `authenticated as`("John", "Wheeler")
`with body`(""" `with body`(
{ """
"name":"La ratatouille", {
"description":"Une petite souris" "name":"La ratatouille",
} "description":"Une petite souris"
""") }
"""
)
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain`("$.id", "aa875a24-0050-4252-9130-d37391714e26") `And the response should contain`("$.id", "aa875a24-0050-4252-9130-d37391714e26")
`And the response should contain`("$.name", "La ratatouille") `And the response should contain`("$.name", "La ratatouille")
`And the response should contain`("$.description", "Une petite souris") `And the response should contain`("$.description", "Une petite souris")
`And have property`("$.members") `And have property`("$.members")
`And the response should contain list`("$.members", 3, 3) `And the response should contain list`("$.members", 3)
`And the response should contain`("$.members.[1]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9") `And the response should contain`("$.members.[1]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9")
`And the response should contain`("$.members.[2]citizen.id", "87909ba3-2069-431c-9924-219fd8411cf2") `And the response should contain`("$.members.[2]citizen.id", "87909ba3-2069-431c-9924-219fd8411cf2")
} }
@@ -171,18 +175,20 @@ class `Workgroup routes` : BaseTest() {
`Given I have workgroup`("b0ea1922-3bc6-44e2-aa7c-40158998cfbb", createdBy = Name("Blaise", "Pascal")) `Given I have workgroup`("b0ea1922-3bc6-44e2-aa7c-40158998cfbb", createdBy = Name("Blaise", "Pascal"))
`When I send a POST request`("/workgroups/b0ea1922-3bc6-44e2-aa7c-40158998cfbb/members") { `When I send a POST request`("/workgroups/b0ea1922-3bc6-44e2-aa7c-40158998cfbb/members") {
`authenticated as`("Blaise", "Pascal") `authenticated as`("Blaise", "Pascal")
`with body`(""" `with body`(
[ """
{ [
"citizen": {"id":"6d883fe7-5fc0-4a50-8858-72230673eba4"}, {
"roles": ["MASTER"] "citizen": {"id":"6d883fe7-5fc0-4a50-8858-72230673eba4"},
}, "roles": ["MASTER"]
{ },
"citizen": {"id":"b5bac515-45d4-4aeb-9b6d-2627a0bbc419"}, {
"roles": ["MASTER"] "citizen": {"id":"b5bac515-45d4-4aeb-9b6d-2627a0bbc419"},
} "roles": ["MASTER"]
] }
""") ]
"""
)
} `Then the response should be` Created } `Then the response should be` Created
} }
} }
@@ -209,7 +215,7 @@ class `Workgroup routes` : BaseTest() {
] ]
""" """
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain list`("$", 2, 2) `And the response should contain list`("$", 2)
`And the response should contain`("$.[0]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9") `And the response should contain`("$.[0]citizen.id", "94f92424-c257-4582-907c-98564a8c4ac9")
`And the response should contain`("$.[1]citizen.id", "1baf48bb-02bc-4d8f-ac86-33335354f5e7") `And the response should contain`("$.[1]citizen.id", "1baf48bb-02bc-4d8f-ac86-33335354f5e7")
} }
@@ -231,20 +237,22 @@ class `Workgroup routes` : BaseTest() {
} }
`When I send a PUT request`("/workgroups/784fe6bc-7635-4ae2-b080-3a4743b998bf/members") { `When I send a PUT request`("/workgroups/784fe6bc-7635-4ae2-b080-3a4743b998bf/members") {
`authenticated as`("Leon", "Foucault") `authenticated as`("Leon", "Foucault")
`with body`(""" `with body`(
[ """
{ [
"citizen": {"id":"be3b0926-8628-4426-804a-75188a6eb315"}, {
"roles": ["MASTER"] "citizen": {"id":"be3b0926-8628-4426-804a-75188a6eb315"},
}, "roles": ["MASTER"]
{ },
"citizen": {"id":"b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1"}, {
"roles": ["MASTER"] "citizen": {"id":"b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1"},
} "roles": ["MASTER"]
] }
""") ]
"""
)
} `Then the response should be` OK and { } `Then the response should be` OK and {
`And the response should contain list`("$", 2, 2) `And the response should contain list`("$", 2)
`And the response should contain`("$.[0]citizen.id", "be3b0926-8628-4426-804a-75188a6eb315") `And the response should contain`("$.[0]citizen.id", "be3b0926-8628-4426-804a-75188a6eb315")
`And the response should contain`("$.[1]citizen.id", "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1") `And the response should contain`("$.[1]citizen.id", "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1")
} }

View File

@@ -15,10 +15,11 @@ fun TestApplicationRequest.`authenticated as`(
val username = "$firstName-$lastName".toLowerCase() val username = "$firstName-$lastName".toLowerCase()
val repo: CitizenRepository by lazy<CitizenRepository> { GlobalContext.get().koin.get() } val repo: CitizenRepository by lazy<CitizenRepository> { GlobalContext.get().koin.get() }
val citizen = repo.findByUsername(username) ?: error("Citizen not exist with username $username") val citizen = repo.findByUsername(username) ?: error("Citizen not exist with username $username")
val algorithm = GlobalContext.get().koin.get<JwtConfig>().algorithm
val jwtAsString: String = JWT.create() val jwtAsString: String = JWT.create()
.withIssuer("dc-project.fr") .withIssuer("dc-project.fr")
.withClaim("id", citizen.user.id.toString()) .withClaim("id", citizen.user.id.toString())
.sign(JwtConfig.algorithm) .sign(algorithm)
addHeader(HttpHeaders.Authorization, "Bearer $jwtAsString") addHeader(HttpHeaders.Authorization, "Bearer $jwtAsString")

View File

@@ -1,7 +1,6 @@
package integration.steps.then package integration.steps.then
import assert.assertGreaterThan import assert.assertContain
import assert.assertLessThan
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.PathNotFoundException import com.jayway.jsonpath.PathNotFoundException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -85,15 +84,13 @@ fun TestApplicationResponse.`And the response should contain pattern`(path: Stri
} }
} }
fun TestApplicationResponse.`And the response should contain list`(path: String, min: Int? = null, max: Int? = null) { fun TestApplicationResponse.`And the response should contain list`(path: String, exactCount: Int) =
`And the response should contain list`(path, IntRange(exactCount, exactCount))
fun TestApplicationResponse.`And the response should contain list`(path: String, range: IntRange) {
JsonPath.read<JSONArray?>(content, path).also { JsonPath.read<JSONArray?>(content, path).also {
assertNotNull(it) assertNotNull(it)
if (min != null) { range assertContain it.size
it.size assertGreaterThan min
}
if (max != null) {
it.size assertLessThan max
}
} }
} }

View File

@@ -2,8 +2,12 @@ package integration.steps.then
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.BooleanNode
import com.fasterxml.jackson.databind.node.IntNode
import com.fasterxml.jackson.databind.node.TextNode import com.fasterxml.jackson.databind.node.TextNode
import fr.dcproject.common.utils.getResource import fr.dcproject.common.utils.getResource
import fr.dcproject.common.utils.isBool
import fr.dcproject.common.utils.isInt
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.Url import io.ktor.http.Url
import io.ktor.request.contentType import io.ktor.request.contentType
@@ -33,7 +37,7 @@ fun Schema.validate(api: OpenApi3, toValidate: JsonNode) {
} }
fun TestApplicationResponse.operation(route: String? = null, callback: Operation.(OpenApi3, String) -> Unit): Operation { fun TestApplicationResponse.operation(route: String? = null, callback: Operation.(OpenApi3, String) -> Unit): Operation {
val filePath = "/openapi2.yaml" val filePath = "/openapi.yaml"
return OpenApi3Parser().parse(File(filePath.getResource().toURI()), true).let { api: OpenApi3 -> return OpenApi3Parser().parse(File(filePath.getResource().toURI()), true).let { api: OpenApi3 ->
val httpMethod = call.request.httpMethod val httpMethod = call.request.httpMethod
val uri = route ?: "/" + Url(call.request.uri).encodedPath val uri = route ?: "/" + Url(call.request.uri).encodedPath
@@ -55,16 +59,17 @@ fun TestApplicationResponse.`And the schema response body must be valid`(content
/* Validate Response */ /* Validate Response */
this.apply { this.apply {
val status = call.response.status() val status = call.response.status()
val httpMethod = call.request.httpMethod.value.toUpperCase()
val responseContent: JsonNode = if (content != null) val responseContent: JsonNode = if (content != null)
ObjectMapper().readTree(content) ObjectMapper().readTree(content)
else TextNode("") else TextNode("")
val response = getResponse(status?.value?.toString() ?: error("HttpStatus not found")) ?: fail("""No Status "${status.value}" found for "$this $uri".""") val response = getResponse(status?.value?.toString() ?: error("HttpStatus not found")) ?: fail("""No Status "${status.value}" found for "$httpMethod $uri".""")
val schema = response.getContentMediaType(contentType.toString())?.schema val schema = response.getContentMediaType(contentType.toString())?.schema
if (content != null) { if (content != null) {
schema?.validate(api, responseContent) schema?.validate(api, responseContent)
?: fail("""No Status "${status.value}" found with media type "$contentType" for "$this $uri".""") ?: fail("""No Status "${status.value}" found with media type "$contentType" for "$httpMethod $uri".""")
} }
} }
} }
@@ -74,13 +79,18 @@ fun TestApplicationResponse.`And the schema parameters must be valid`() {
operation { api, uri -> operation { api, uri ->
/* Validate Request URL */ /* Validate Request URL */
this.apply { this.apply {
val methodName = call.request.httpMethod.value.toUpperCase()
Url(call.request.uri).parameters.forEach { parameter: String, values: List<String> -> Url(call.request.uri).parameters.forEach { parameter: String, values: List<String> ->
val schema = getParametersIn(api.context, "query") val schema = getParametersIn(api.context, "query")
?.firstOrNull { it.name == parameter }?.schema ?.firstOrNull { it.name == parameter }?.schema
?: error("""No parameter found ($parameter) for "$this $uri".""") ?: error("""No parameter found ($parameter) for "$methodName $uri".""")
if (schema.type == "array") { if (schema.type == "array") {
schema.validate(api, ObjectMapper().valueToTree(values)) schema.validate(api, ObjectMapper().valueToTree(values))
} else if (schema.type == "integer" && values.first().isInt()) {
schema.validate(api, IntNode(values.first().toInt()))
} else if (schema.type == "boolean" && values.first().isBool()) {
schema.validate(api, BooleanNode.valueOf(values.first().toBoolean()))
} else { } else {
schema.validate(api, TextNode(values.first())) schema.validate(api, TextNode(values.first()))
} }

View File

@@ -27,7 +27,7 @@ import fr.dcproject.component.article.database.ArticleRepository as ArticleRepo
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"), Tag("article"))
internal class `Article Access Control` { internal class `Article Access Control` {
private val tesla = CitizenCreator( private val tesla = CitizenCreator(
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"), id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),

View File

@@ -18,7 +18,7 @@ import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"), Tag("citizen"))
internal class `Citizen Access Control` { internal class `Citizen Access Control` {
private val tesla = CitizenCart( private val tesla = CitizenCart(
user = User( user = User(

View File

@@ -25,7 +25,7 @@ import java.util.UUID
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"), Tag("comment"))
internal class `Comment Access Control` { internal class `Comment Access Control` {
private val tesla = Citizen( private val tesla = Citizen(
user = User( user = User(

View File

@@ -23,7 +23,7 @@ import java.util.UUID
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"), Tag("follow"))
internal class `Follow Access Control` { internal class `Follow Access Control` {
private val tesla = CitizenCreator( private val tesla = CitizenCreator(
user = UserCreator( user = UserCreator(

View File

@@ -21,7 +21,7 @@ import java.util.UUID
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"), Tag("opinion"))
internal class `Opinion Access Control` { internal class `Opinion Access Control` {
private val tesla = CitizenCreator( private val tesla = CitizenCreator(
user = UserCreator( user = UserCreator(

View File

@@ -15,7 +15,7 @@ import java.util.UUID
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"), Tag("opinion"))
internal class `OpinionChoice Access Control` { internal class `OpinionChoice Access Control` {
private val tesla = CitizenRef( private val tesla = CitizenRef(
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"), id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),

View File

@@ -11,6 +11,7 @@ import fr.dcproject.component.citizen.database.CitizenCreator
import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenI
import fr.dcproject.component.vote.VoteAccessControl import fr.dcproject.component.vote.VoteAccessControl
import fr.dcproject.component.vote.database.VoteForUpdate import fr.dcproject.component.vote.database.VoteForUpdate
import fr.dcproject.component.vote.database.VoteForView
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
@@ -20,11 +21,10 @@ import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
import java.util.UUID import java.util.UUID
import fr.dcproject.component.vote.database.VoteForView
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"), Tag("vote"))
internal class `Vote Access Control` { internal class `Vote Access Control` {
private val tesla = Citizen( private val tesla = Citizen(
id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"), id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"),

View File

@@ -20,7 +20,7 @@ import fr.dcproject.component.workgroup.database.WorkgroupForView as WorkgroupEn
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tags(Tag("security"), Tag("unit")) @Tags(Tag("security"), Tag("unit"), Tag("workgroup"))
internal class `Workgroup Access Control` { internal class `Workgroup Access Control` {
private val tesla = CitizenCreator( private val tesla = CitizenCreator(
user = UserCreator( user = UserCreator(

View File

@@ -37,3 +37,9 @@ mail {
key = "abcd" key = "abcd"
} }
} }
jwt {
secret = "zAP5MBA4B4Ijz0MZaS48"
issuer = "dc-project.fr"
validity = 36000000
}

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