diff --git a/build.gradle.kts b/build.gradle.kts index b8105df..df8a3ee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -426,7 +426,7 @@ dependencies { testImplementation("io.insert-koin:koin-test:+") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:+") testImplementation("io.mockk:mockk:+") - testImplementation("org.junit.jupiter:junit-jupiter:+") + testImplementation("org.junit.jupiter:junit-jupiter:5.7.+") testImplementation("org.amshove.kluent:kluent:+") testImplementation("io.mockk:mockk-agent-api:+") testImplementation("io.mockk:mockk-agent-jvm:+") diff --git a/gradle.lockfile b/gradle.lockfile index 5bfd56e..35c296d 100644 --- a/gradle.lockfile +++ b/gradle.lockfile @@ -1,277 +1,131 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -ch.qos.logback:logback-classic:1.3.0-alpha5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -ch.qos.logback:logback-core:1.3.0-alpha5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.auth0:java-jwt:3.15.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.auth0:jwks-rsa:0.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.avast.gradle:gradle-docker-compose-plugin:0.14.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.beust:jcommander:1.81=detekt -com.fasterxml.jackson.core:jackson-annotations:2.12.2=detekt -com.fasterxml.jackson.core:jackson-annotations:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-core:2.12.2=detekt -com.fasterxml.jackson.core:jackson-core:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-databind:2.12.2=detekt -com.fasterxml.jackson.core:jackson-databind:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.fasterxml.jackson.datatype:jackson-datatype-joda:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.fasterxml.jackson:jackson-bom:2.12.2=detekt -com.fasterxml.jackson:jackson-bom:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.github.flecomte:postgres-json:2.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.github.jasync-sql:jasync-common:1.1.7=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.github.jasync-sql:jasync-pool:1.1.7=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.github.jasync-sql:jasync-postgresql:1.1.7=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.github.shyiko.klob:klob:0.2.1=ktlint -com.google.code.findbugs:jsr305:3.0.2=runtimeClasspath,testRuntimeClasspath -com.google.errorprone:error_prone_annotations:2.2.0=runtimeClasspath,testRuntimeClasspath -com.google.guava:failureaccess:1.0.1=runtimeClasspath,testRuntimeClasspath -com.google.guava:guava:27.1-jre=runtimeClasspath,testRuntimeClasspath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=runtimeClasspath,testRuntimeClasspath -com.google.j2objc:j2objc-annotations:1.1=runtimeClasspath,testRuntimeClasspath -com.googlecode.json-simple:json-simple:1.1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.jayway.jsonpath:json-path:2.5.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.ongres.scram:client:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.ongres.scram:common:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.ongres.stringprep:saslprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.ongres.stringprep:stringprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.pinterest.ktlint:ktlint-core:0.40.0=ktlint -com.pinterest.ktlint:ktlint-reporter-baseline:0.40.0=ktlint -com.pinterest.ktlint:ktlint-reporter-checkstyle:0.40.0=ktlint -com.pinterest.ktlint:ktlint-reporter-html:0.40.0=ktlint -com.pinterest.ktlint:ktlint-reporter-json:0.40.0=ktlint -com.pinterest.ktlint:ktlint-reporter-plain:0.40.0=ktlint -com.pinterest.ktlint:ktlint-ruleset-experimental:0.40.0=ktlint -com.pinterest.ktlint:ktlint-ruleset-standard:0.40.0=ktlint -com.pinterest.ktlint:ktlint-ruleset-test:0.40.0=ktlint -com.pinterest:ktlint:0.40.0=ktlint -com.rabbitmq:amqp-client:5.12.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.sendgrid:java-http-client:4.3.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.sendgrid:sendgrid-java:4.7.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.sun.mail:javax.mail:1.6.2=runtimeClasspath,testRuntimeClasspath -com.thedeanda:lorem:2.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -com.typesafe:config:1.3.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-codec:commons-codec:1.11=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata -commons-codec:commons-codec:1.13=runtimeClasspath,testRuntimeClasspath -commons-io:commons-io:2.6=runtimeClasspath,testRuntimeClasspath -commons-logging:commons-logging:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -edu.washington.cs.types.checker:checker-framework:1.7.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -info.picocli:picocli:3.9.6=ktlint -io.github.detekt.sarif4j:sarif4j:1.0.0=detekt -io.github.microutils:kotlin-logging:1.7.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.gitlab.arturbosch.detekt:detekt-api:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-bom:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-cli:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-core:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-metrics:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-parser:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-psi-utils:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-report-html:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-report-sarif:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-report-txt:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-report-xml:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-complexity:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-coroutines:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-documentation:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-empty:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-errorprone:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-exceptions:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-naming:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-performance:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules-style:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-rules:1.16.0=detekt -io.gitlab.arturbosch.detekt:detekt-tooling:1.16.0=detekt -io.insert-koin:koin-core-ext:3.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.insert-koin:koin-core-jvm:3.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.insert-koin:koin-core:3.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.insert-koin:koin-ktor:3.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.insert-koin:koin-test-jvm:3.0.1=testCompileClasspath,testRuntimeClasspath -io.insert-koin:koin-test:3.0.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.konform:konform-jvm:0.3.0-RC1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.konform:konform-metadata:0.3.0-RC1=implementationDependenciesMetadata,testImplementationDependenciesMetadata -io.ktor:ktor-auth-jwt-kotlinMultiplatform:1.5.3=implementationDependenciesMetadata,testImplementationDependenciesMetadata -io.ktor:ktor-auth-jwt:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-auth-kotlinMultiplatform:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-auth:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-client-cio-jvm:1.5.3=testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-client-cio:1.5.3=testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-client-core-jvm:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-client-core:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-client-jetty-kotlinMultiplatform:1.5.3=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-client-jetty:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-client-mock-jvm:1.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-client-mock:1.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-http-cio-jvm:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-http-cio:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-http-jvm:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-http:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-io-jvm:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-io:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-jackson-kotlinMultiplatform:1.5.3=implementationDependenciesMetadata,testImplementationDependenciesMetadata -io.ktor:ktor-jackson:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-locations-kotlinMultiplatform:1.5.3=implementationDependenciesMetadata,testImplementationDependenciesMetadata -io.ktor:ktor-locations:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-network-jvm:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-network-tls-certificates-kotlinMultiplatform:1.5.3=testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-network-tls-certificates:1.5.3=testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-network-tls-jvm:1.5.3=testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-network-tls:1.5.3=testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-network:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-server-core-kotlinMultiplatform:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-server-core:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-server-host-common-kotlinMultiplatform:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-server-host-common:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-server-jetty-kotlinMultiplatform:1.5.3=implementationDependenciesMetadata,testImplementationDependenciesMetadata -io.ktor:ktor-server-jetty:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-server-servlet-kotlinMultiplatform:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-server-servlet:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-server-test-host-kotlinMultiplatform:1.5.3=testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-server-test-host:1.5.3=testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-server-tests-kotlinMultiplatform:1.5.3=testImplementationDependenciesMetadata -io.ktor:ktor-server-tests:1.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-utils-jvm:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.ktor:ktor-utils:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-websockets-kotlinMultiplatform:1.5.3=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.ktor:ktor-websockets:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.lettuce:lettuce-core:5.3.6.RELEASE=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.mockk:mockk-agent-api:1.11.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.mockk:mockk-agent-common:1.11.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.mockk:mockk-agent-jvm:1.11.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.mockk:mockk-common:1.11.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.mockk:mockk-dsl-jvm:1.11.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.mockk:mockk-dsl:1.11.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.mockk:mockk:1.11.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.netty:netty-buffer:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.netty:netty-codec:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.netty:netty-common:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.netty:netty-handler:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.netty:netty-resolver:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.netty:netty-transport:4.1.56.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -io.projectreactor:reactor-core:3.3.13.RELEASE=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -javax.activation:activation:1.1=runtimeClasspath,testRuntimeClasspath -javax.servlet:javax.servlet-api:3.1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -joda-time:joda-time:2.10.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -junit:junit:4.13.1=testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy-agent:1.10.14=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.10.14=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -net.java.dev.jna:jna-platform:5.5.0=testRuntimeClasspath -net.java.dev.jna:jna:5.5.0=testRuntimeClasspath -net.minidev:accessors-smart:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -net.minidev:json-smart:2.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -net.pearx.kasechange:kasechange-jvm:1.3.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -net.pearx.kasechange:kasechange-metadata:1.3.0=implementationDependenciesMetadata,testImplementationDependenciesMetadata -org.amshove.kluent:kluent-common:1.65=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.amshove.kluent:kluent:1.65=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.apache.httpcomponents:httpasyncclient:4.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.apache.httpcomponents:httpclient:4.5.12=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.apache.httpcomponents:httpcore-nio:4.4.5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.apache.httpcomponents:httpcore:4.4.13=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.apiguardian:apiguardian-api:1.1.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.bouncycastle:bcprov-jdk15on:1.67=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.checkerframework:checker-qual:2.5.2=runtimeClasspath,testRuntimeClasspath -org.codehaus.mojo:animal-sniffer-annotations:1.17=runtimeClasspath,testRuntimeClasspath -org.ec4j.core:ec4j-core:0.2.2=ktlint -org.eclipse.jetty.http2:http2-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:http2-common:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:http2-hpack:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:http2-http-client-transport:9.4.31.v20200723=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:http2-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-java-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-java-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-openjdk8-client:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-openjdk8-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-client:9.4.31.v20200723=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-continuation:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-http:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-io:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-server:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-servlets:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-util:9.4.31.v20200723=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.elasticsearch.client:elasticsearch-rest-client:6.8.15=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath -org.jacoco:org.jacoco.agent:0.8.6=jacocoAgent,jacocoAnt -org.jacoco:org.jacoco.ant:0.8.6=jacocoAnt -org.jacoco:org.jacoco.core:0.8.6=jacocoAnt -org.jacoco:org.jacoco.report:0.8.6=jacocoAnt -org.jetbrains.intellij.deps:trove4j:1.0.20181211=detekt,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,ktlint -org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.10=ktlint -org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.21=detekt -org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.32=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath -org.jetbrains.kotlin:kotlin-daemon-embeddable:1.4.10=ktlint -org.jetbrains.kotlin:kotlin-daemon-embeddable:1.4.21=detekt -org.jetbrains.kotlin:kotlin-daemon-embeddable:1.4.32=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +ch.qos.logback:logback-classic:1.3.0-alpha5=compileClasspath +ch.qos.logback:logback-core:1.3.0-alpha5=compileClasspath +com.auth0:java-jwt:3.15.0=compileClasspath +com.auth0:jwks-rsa:0.9.0=compileClasspath +com.avast.gradle:gradle-docker-compose-plugin:0.14.3=compileClasspath +com.fasterxml.jackson.core:jackson-annotations:2.12.3=compileClasspath +com.fasterxml.jackson.core:jackson-core:2.12.3=compileClasspath +com.fasterxml.jackson.core:jackson-databind:2.12.3=compileClasspath +com.fasterxml.jackson.datatype:jackson-datatype-joda:2.12.3=compileClasspath +com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3=compileClasspath +com.fasterxml.jackson:jackson-bom:2.12.3=compileClasspath +com.github.flecomte:postgres-json:2.1.2=compileClasspath +com.github.jasync-sql:jasync-common:1.1.7=compileClasspath +com.github.jasync-sql:jasync-pool:1.1.7=compileClasspath +com.github.jasync-sql:jasync-postgresql:1.1.7=compileClasspath +com.googlecode.json-simple:json-simple:1.1.1=compileClasspath +com.jayway.jsonpath:json-path:2.5.0=compileClasspath +com.ongres.scram:client:2.1=compileClasspath +com.ongres.scram:common:2.1=compileClasspath +com.ongres.stringprep:saslprep:1.1=compileClasspath +com.ongres.stringprep:stringprep:1.1=compileClasspath +com.rabbitmq:amqp-client:5.12.0=compileClasspath +com.sendgrid:java-http-client:4.3.6=compileClasspath +com.sendgrid:sendgrid-java:4.7.1=compileClasspath +com.typesafe:config:1.3.1=compileClasspath +commons-codec:commons-codec:1.11=compileClasspath +commons-logging:commons-logging:1.2=compileClasspath +edu.washington.cs.types.checker:checker-framework:1.7.0=compileClasspath +io.github.microutils:kotlin-logging:1.7.6=compileClasspath +io.insert-koin:koin-core-ext:3.0.1=compileClasspath +io.insert-koin:koin-core-jvm:3.0.1=compileClasspath +io.insert-koin:koin-core:3.0.1=compileClasspath +io.insert-koin:koin-ktor:3.0.1=compileClasspath +io.konform:konform-jvm:0.3.0-RC1=compileClasspath +io.ktor:ktor-auth-jwt:1.5.3=compileClasspath +io.ktor:ktor-auth-kotlinMultiplatform:1.5.3=compileClasspath +io.ktor:ktor-auth:1.5.3=compileClasspath +io.ktor:ktor-client-core-jvm:1.5.3=compileClasspath +io.ktor:ktor-client-core:1.5.3=compileClasspath +io.ktor:ktor-client-jetty:1.5.3=compileClasspath +io.ktor:ktor-http-cio-jvm:1.5.3=compileClasspath +io.ktor:ktor-http-cio:1.5.3=compileClasspath +io.ktor:ktor-http-jvm:1.5.3=compileClasspath +io.ktor:ktor-http:1.5.3=compileClasspath +io.ktor:ktor-io-jvm:1.5.3=compileClasspath +io.ktor:ktor-io:1.5.3=compileClasspath +io.ktor:ktor-jackson:1.5.3=compileClasspath +io.ktor:ktor-locations:1.5.3=compileClasspath +io.ktor:ktor-network-jvm:1.5.3=compileClasspath +io.ktor:ktor-network:1.5.3=compileClasspath +io.ktor:ktor-server-core-kotlinMultiplatform:1.5.3=compileClasspath +io.ktor:ktor-server-core:1.5.3=compileClasspath +io.ktor:ktor-server-host-common-kotlinMultiplatform:1.5.3=compileClasspath +io.ktor:ktor-server-host-common:1.5.3=compileClasspath +io.ktor:ktor-server-jetty:1.5.3=compileClasspath +io.ktor:ktor-server-servlet-kotlinMultiplatform:1.5.3=compileClasspath +io.ktor:ktor-server-servlet:1.5.3=compileClasspath +io.ktor:ktor-utils-jvm:1.5.3=compileClasspath +io.ktor:ktor-utils:1.5.3=compileClasspath +io.ktor:ktor-websockets:1.5.3=compileClasspath +io.lettuce:lettuce-core:5.3.6.RELEASE=compileClasspath +io.netty:netty-buffer:4.1.56.Final=compileClasspath +io.netty:netty-codec:4.1.56.Final=compileClasspath +io.netty:netty-common:4.1.56.Final=compileClasspath +io.netty:netty-handler:4.1.56.Final=compileClasspath +io.netty:netty-resolver:4.1.56.Final=compileClasspath +io.netty:netty-transport:4.1.56.Final=compileClasspath +io.projectreactor:reactor-core:3.4.1=compileClasspath +javax.servlet:javax.servlet-api:3.1.0=compileClasspath +joda-time:joda-time:2.10.8=compileClasspath +net.minidev:accessors-smart:1.2=compileClasspath +net.minidev:json-smart:2.3=compileClasspath +net.pearx.kasechange:kasechange-jvm:1.3.0=compileClasspath +org.apache.httpcomponents:httpasyncclient:4.1.2=compileClasspath +org.apache.httpcomponents:httpclient:4.5.12=compileClasspath +org.apache.httpcomponents:httpcore-nio:4.4.5=compileClasspath +org.apache.httpcomponents:httpcore:4.4.13=compileClasspath +org.bouncycastle:bcprov-jdk15on:1.67=compileClasspath +org.eclipse.jetty.http2:http2-client:9.4.31.v20200723=compileClasspath +org.eclipse.jetty.http2:http2-common:9.4.31.v20200723=compileClasspath +org.eclipse.jetty.http2:http2-hpack:9.4.31.v20200723=compileClasspath +org.eclipse.jetty.http2:http2-server:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-alpn-client:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-alpn-java-client:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-alpn-java-server:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-alpn-openjdk8-client:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-alpn-openjdk8-server:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-alpn-server:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-continuation:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-http:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-io:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-server:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-servlets:9.4.31.v20200723=compileClasspath +org.eclipse.jetty:jetty-util:9.4.31.v20200723=compileClasspath +org.elasticsearch.client:elasticsearch-rest-client:6.8.15=compileClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20181211=kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.32=kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.4.32=kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.4.32=kotlinCompilerPluginClasspath -org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.4.32=kotlinKlibCommonizerClasspath -org.jetbrains.kotlin:kotlin-reflect:1.4.10=ktlint -org.jetbrains.kotlin:kotlin-reflect:1.4.21=detekt,implementationDependenciesMetadata -org.jetbrains.kotlin:kotlin-reflect:1.4.32=compileClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlin:kotlin-script-runtime:1.4.10=ktlint -org.jetbrains.kotlin:kotlin-script-runtime:1.4.21=detekt -org.jetbrains.kotlin:kotlin-script-runtime:1.4.32=kotlinCompilerClasspath,kotlinCompilerPluginClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.4.32=compileClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.4.32=kotlinCompilerClasspath,kotlinCompilerPluginClasspath org.jetbrains.kotlin:kotlin-scripting-common:1.4.32=kotlinCompilerPluginClasspath org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.4.32=kotlinCompilerPluginClasspath org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.4.32=kotlinCompilerPluginClasspath org.jetbrains.kotlin:kotlin-scripting-jvm:1.4.32=kotlinCompilerPluginClasspath -org.jetbrains.kotlin:kotlin-serialization-unshaded:1.4.32=kotlinNativeCompilerPluginClasspath org.jetbrains.kotlin:kotlin-serialization:1.4.32=kotlinCompilerPluginClasspath -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10=ktlint -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21=detekt -org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32=compileClasspath,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspath,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21=detekt -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.32=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21=detekt -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib:1.4.10=ktlint -org.jetbrains.kotlin:kotlin-stdlib:1.4.21=detekt -org.jetbrains.kotlin:kotlin-stdlib:1.4.32=compileClasspath,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspath,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlin:kotlin-test-annotations-common:1.4.30=testImplementationDependenciesMetadata -org.jetbrains.kotlin:kotlin-test-annotations-common:1.4.32=testCompileClasspath,testRuntimeClasspath -org.jetbrains.kotlin:kotlin-test-common:1.4.30=testImplementationDependenciesMetadata -org.jetbrains.kotlin:kotlin-test-common:1.4.32=testCompileClasspath,testRuntimeClasspath -org.jetbrains.kotlin:kotlin-test-junit:1.4.32=testCompileClasspath,testRuntimeClasspath -org.jetbrains.kotlin:kotlin-test:1.4.32=testCompileClasspath,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.2=implementationDependenciesMetadata,testImplementationDependenciesMetadata -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.3-native-mt=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-core-metadata:1.4.2=implementationDependenciesMetadata,testImplementationDependenciesMetadata +org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32=kotlinCompilerClasspath,kotlinCompilerPluginClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.5.0=compileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.0=compileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.0=compileClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.4.32=kotlinCompilerClasspath,kotlinCompilerPluginClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.5.0=compileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0-RC=compileClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8=kotlinCompilerPluginClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2=implementationDependenciesMetadata -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.4.3-native-mt=testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3-native-mt=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.4.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.4.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2=detekt -org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-core-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata -org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-json-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata -org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.jetbrains:annotations:13.0=compileClasspath,detekt,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspath,kotlinKlibCommonizerClasspath,ktlint,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.joda:joda-convert:1.8.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.8.0-M1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.8.0-M1=testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.8.0-M1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.junit.jupiter:junit-jupiter:5.8.0-M1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.8.0-M1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.8.0-M1=testRuntimeClasspath -org.junit:junit-bom:5.8.0-M1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.objenesis:objenesis:3.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.openapi4j:openapi-core:1.0.7=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.openapi4j:openapi-operation-validator:1.0.7=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.openapi4j:openapi-parser:1.0.7=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.openapi4j:openapi-schema-validator:1.0.7=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.ow2.asm:asm-analysis:8.0.1=jacocoAnt -org.ow2.asm:asm-commons:8.0.1=jacocoAnt -org.ow2.asm:asm-tree:8.0.1=jacocoAnt -org.ow2.asm:asm:5.0.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.ow2.asm:asm:8.0.1=jacocoAnt -org.reactivestreams:reactive-streams:1.0.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.slf4j:slf4j-api:2.0.0-alpha1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath -org.yaml:snakeyaml:1.27=testCompileClasspath,testImplementationDependenciesMetadata -org.yaml:snakeyaml:1.28=detekt,runtimeClasspath,testRuntimeClasspath -empty=annotationProcessor,apiDependenciesMetadata,compile,compileOnly,compileOnlyDependenciesMetadata,detektPlugins,kotlinScriptDef,kotlinScriptDefExtensions,ktlintReporter,ktlintRuleset,runtime,runtimeOnlyDependenciesMetadata,shadow,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0-RC=compileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3-native-mt=compileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.0-RC=compileClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.5.0-RC=compileClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.2.0=compileClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core:1.2.0=compileClasspath +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.2.0=compileClasspath +org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0=compileClasspath +org.jetbrains:annotations:13.0=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspath +org.joda:joda-convert:1.8.1=compileClasspath +org.ow2.asm:asm:5.0.4=compileClasspath +org.reactivestreams:reactive-streams:1.0.3=compileClasspath +org.slf4j:slf4j-api:2.0.0-alpha1=compileClasspath +empty= diff --git a/src/main/kotlin/fr/dcproject/application/Application.kt b/src/main/kotlin/fr/dcproject/application/Application.kt index 539a645..470e4e5 100644 --- a/src/main/kotlin/fr/dcproject/application/Application.kt +++ b/src/main/kotlin/fr/dcproject/application/Application.kt @@ -9,6 +9,7 @@ import com.fasterxml.jackson.datatype.joda.JodaModule import fr.dcproject.application.Env.PROD import fr.dcproject.application.Env.TEST import fr.dcproject.application.http.statusPagesInstallation +import fr.dcproject.common.utils.onApplicationStopped import fr.dcproject.component.article.articleKoinModule import fr.dcproject.component.article.routes.installArticleRoutes import fr.dcproject.component.auth.authKoinModule @@ -25,8 +26,10 @@ import fr.dcproject.component.constitution.routes.installConstitutionRoutes import fr.dcproject.component.doc.routes.installDocRoutes import fr.dcproject.component.follow.followKoinModule import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes +import fr.dcproject.component.follow.routes.citizen.installFollowCitizenRoutes import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes -import fr.dcproject.component.notification.NotificationConsumer +import fr.dcproject.component.notification.email.NotificationEmailConsumer +import fr.dcproject.component.notification.push.NotificationPushConsumer import fr.dcproject.component.notification.routes.installNotificationsRoutes import fr.dcproject.component.opinion.opinionKoinModule import fr.dcproject.component.opinion.routes.installOpinionRoutes @@ -37,7 +40,6 @@ import fr.dcproject.component.workgroup.routes.installWorkgroupRoutes import fr.dcproject.component.workgroup.workgroupKoinModule import fr.postgresjson.migration.Migrations import io.ktor.application.Application -import io.ktor.application.ApplicationStopped import io.ktor.application.install import io.ktor.auth.Authentication import io.ktor.client.HttpClient @@ -117,11 +119,14 @@ fun Application.module(env: Env = PROD) { masking = false } - get().run { + get().run { start() - environment.monitor.subscribe(ApplicationStopped) { - close() - } + onApplicationStopped { close() } + } + + get().run { + start() + onApplicationStopped { close() } } install(Authentication, jwtInstallation(get(), get())) @@ -154,6 +159,7 @@ fun Application.module(env: Env = PROD) { installCommentRoutes() installFollowArticleRoutes() installFollowConstitutionRoutes() + installFollowCitizenRoutes() installWorkgroupRoutes() installOpinionRoutes() installVoteRoutes() diff --git a/src/main/kotlin/fr/dcproject/application/KoinModule.kt b/src/main/kotlin/fr/dcproject/application/KoinModule.kt index f2e88e8..2867e3a 100644 --- a/src/main/kotlin/fr/dcproject/application/KoinModule.kt +++ b/src/main/kotlin/fr/dcproject/application/KoinModule.kt @@ -10,10 +10,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.rabbitmq.client.ConnectionFactory import fr.dcproject.common.email.Mailer import fr.dcproject.component.auth.jwt.JwtConfig -import fr.dcproject.component.notification.NotificationConsumer -import fr.dcproject.component.notification.NotificationEmailSender -import fr.dcproject.component.notification.NotificationsPush -import fr.dcproject.component.notification.Publisher +import fr.dcproject.component.notification.NotificationPublisherAsync +import fr.dcproject.component.notification.email.NotificationEmailConsumer +import fr.dcproject.component.notification.email.NotificationEmailSender +import fr.dcproject.component.notification.push.NotificationPushConsumer +import fr.dcproject.component.notification.push.NotificationPushListener import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Requester import fr.postgresjson.migration.Migrations @@ -65,11 +66,15 @@ val KoinModule = module { } } - single { NotificationsPush.Builder(get()) } + single { NotificationPushListener.Builder(get()) } single { val config: Configuration = get() - NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName) + NotificationEmailConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName) + } + single { + val config: Configuration = get() + NotificationPushConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName) } // RabbitMQ @@ -114,7 +119,7 @@ val KoinModule = module { single { val config: Configuration = get() - Publisher(factory = get(), exchangeName = config.exchangeNotificationName) + NotificationPublisherAsync(factory = get(), exchangeName = config.exchangeNotificationName) } single { diff --git a/src/main/kotlin/fr/dcproject/common/email/Mailer.kt b/src/main/kotlin/fr/dcproject/common/email/Mailer.kt index 334a283..ee6cebd 100644 --- a/src/main/kotlin/fr/dcproject/common/email/Mailer.kt +++ b/src/main/kotlin/fr/dcproject/common/email/Mailer.kt @@ -9,6 +9,9 @@ import java.io.IOException class Mailer( private val key: String ) { + /** + * Send email via Sendgrid + */ fun sendEmail(action: () -> Mail): Boolean { val mail = action() diff --git a/src/main/kotlin/fr/dcproject/common/entity/Extra.kt b/src/main/kotlin/fr/dcproject/common/entity/Extra.kt index 228235f..0245c05 100644 --- a/src/main/kotlin/fr/dcproject/common/entity/Extra.kt +++ b/src/main/kotlin/fr/dcproject/common/entity/Extra.kt @@ -2,6 +2,7 @@ package fr.dcproject.common.entity import fr.dcproject.component.article.database.ArticleRef import fr.dcproject.component.citizen.database.CitizenI +import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.comment.generic.database.CommentRef import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.opinion.database.OpinionRef @@ -34,7 +35,8 @@ interface TargetI : EntityI { Article("article"), Constitution("constitution"), Comment("comment"), - Opinion("opinion") + Opinion("opinion"), + Citizen("citizen"), } companion object { @@ -44,6 +46,7 @@ interface TargetI : EntityI { t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference + t.isSubclassOf(CitizenRef::class) -> TargetName.Citizen.targetReference else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL") } } diff --git a/src/main/kotlin/fr/dcproject/common/utils/Ktor.kt b/src/main/kotlin/fr/dcproject/common/utils/Ktor.kt new file mode 100644 index 0000000..9883182 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/common/utils/Ktor.kt @@ -0,0 +1,10 @@ +package fr.dcproject.common.utils + +import io.ktor.application.Application +import io.ktor.application.ApplicationStopped + +fun Application.onApplicationStopped(callback: Application.() -> Unit) { + environment.monitor.subscribe(ApplicationStopped) { + callback() + } +} diff --git a/src/main/kotlin/fr/dcproject/common/utils/RabbitConsume.kt b/src/main/kotlin/fr/dcproject/common/utils/RabbitConsume.kt new file mode 100644 index 0000000..d7c57f7 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/common/utils/RabbitConsume.kt @@ -0,0 +1,30 @@ +package fr.dcproject.common.utils + +import com.rabbitmq.client.AMQP +import com.rabbitmq.client.Channel +import com.rabbitmq.client.Consumer +import com.rabbitmq.client.DefaultConsumer +import com.rabbitmq.client.Envelope +import kotlinx.coroutines.runBlocking +import java.io.IOException + +fun Channel.consumeQueue(queueName: String, callback: DefaultConsumer.(ByteArray) -> Unit) { + val consumer: Consumer = object : DefaultConsumer(this) { + @Throws(IOException::class) + override fun handleDelivery( + consumerTag: String, + envelope: Envelope, + properties: AMQP.BasicProperties, + body: ByteArray + ) = runBlocking { + try { + callback(body) + basicAck(envelope.deliveryTag, false) + } catch (e: Throwable) { + basicNack(envelope.deliveryTag, false, true) + } + } + } + /* Launch Consumer */ + basicConsume(queueName, false, consumer) +} diff --git a/src/main/kotlin/fr/dcproject/component/article/routes/UpsertArticle.kt b/src/main/kotlin/fr/dcproject/component/article/routes/UpsertArticle.kt index 57fce8a..8f018f1 100644 --- a/src/main/kotlin/fr/dcproject/component/article/routes/UpsertArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/article/routes/UpsertArticle.kt @@ -10,8 +10,8 @@ import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest. import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.mustBeAuth -import fr.dcproject.component.notification.ArticleUpdateNotification -import fr.dcproject.component.notification.Publisher +import fr.dcproject.component.notification.ArticleUpdateNotificationMessage +import fr.dcproject.component.notification.NotificationPublisherAsync import fr.dcproject.component.workgroup.database.WorkgroupRef import io.konform.validation.Validation import io.konform.validation.jsonschema.maxItems @@ -63,7 +63,7 @@ object UpsertArticle { } } - fun Route.upsertArticle(repo: ArticleRepository, publisher: Publisher, ac: ArticleAccessControl) { + fun Route.upsertArticle(repo: ArticleRepository, notificationPublisher: NotificationPublisherAsync, ac: ArticleAccessControl) { suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest().run { validate().badRequestIfNotValid() ArticleForUpdate( @@ -92,7 +92,7 @@ object UpsertArticle { val versionNumber = a.versionNumber } ) - publisher.publish(ArticleUpdateNotification(a)) + notificationPublisher.publishAsync(ArticleUpdateNotificationMessage(a)) } ?: error("Article not updated") } } diff --git a/src/main/kotlin/fr/dcproject/component/citizen/database/Citizen.kt b/src/main/kotlin/fr/dcproject/component/citizen/database/Citizen.kt index 2b1d251..14d0597 100644 --- a/src/main/kotlin/fr/dcproject/component/citizen/database/Citizen.kt +++ b/src/main/kotlin/fr/dcproject/component/citizen/database/Citizen.kt @@ -2,8 +2,9 @@ package fr.dcproject.component.citizen.database import fr.dcproject.common.entity.CreatedAt import fr.dcproject.common.entity.DeletedAt -import fr.dcproject.common.entity.Entity import fr.dcproject.common.entity.EntityI +import fr.dcproject.common.entity.TargetI +import fr.dcproject.common.entity.TargetRef import fr.dcproject.component.auth.database.User import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.auth.database.UserForCreate @@ -95,10 +96,10 @@ open class CitizenRefWithUser( open class CitizenRef( id: UUID = UUID.randomUUID() -) : Entity(id), +) : TargetRef(id), CitizenI -interface CitizenI : EntityI { +interface CitizenI : EntityI, TargetI { data class Name( override val firstName: String, override val lastName: String, diff --git a/src/main/kotlin/fr/dcproject/component/follow/KoinModule.kt b/src/main/kotlin/fr/dcproject/component/follow/KoinModule.kt index e57738c..a86ac86 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/KoinModule.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/KoinModule.kt @@ -1,11 +1,13 @@ package fr.dcproject.component.follow import fr.dcproject.component.follow.database.FollowArticleRepository +import fr.dcproject.component.follow.database.FollowCitizenRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository import org.koin.dsl.module val followKoinModule = module { single { FollowArticleRepository(get()) } single { FollowConstitutionRepository(get()) } + single { FollowCitizenRepository(get()) } single { FollowAccessControl() } } diff --git a/src/main/kotlin/fr/dcproject/component/follow/database/FollowRepository.kt b/src/main/kotlin/fr/dcproject/component/follow/database/FollowRepository.kt index 7db5aa0..96063ff 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/database/FollowRepository.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/database/FollowRepository.kt @@ -4,7 +4,9 @@ import fr.dcproject.common.entity.Entity import fr.dcproject.common.entity.TargetRef import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.article.database.ArticleRef +import fr.dcproject.component.citizen.database.Citizen import fr.dcproject.component.citizen.database.CitizenI +import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.constitution.database.ConstitutionForView import fr.dcproject.component.constitution.database.ConstitutionRef import fr.postgresjson.connexion.Paginated @@ -72,21 +74,24 @@ sealed class FollowRepository(override var requ target: Entity, bulkSize: Int = 300 ): Flow> = flow { - var nextPage = 1 - do { - val paginate = findFollowsByTarget(target, nextPage, bulkSize) - paginate.result.forEach { + var lastId: UUID? = null + while (true) { + val result = findFollowsByTarget(target, lastId, bulkSize) + if (result.count() == 0) { + break + } + result.forEach { emit(it) } - nextPage = paginate.currentPage + 1 - } while (!paginate.isLastPage()) + lastId = result.last().id + } } abstract fun findFollowsByTarget( target: Entity, - page: Int = 1, + lastId: UUID?, limit: Int = 300 - ): Paginated> + ): List> } class FollowArticleRepository(requester: Requester) : FollowRepository(requester) { @@ -107,14 +112,14 @@ class FollowArticleRepository(requester: Requester) : FollowRepository> { + ): List> { return requester .getFunction("find_follows_article_by_target") .select( - page, - limit, + "start_id" to lastId, + "limit" to limit, "target_id" to target.id ) } @@ -138,9 +143,34 @@ class FollowConstitutionRepository(requester: Requester) : FollowRepository> { + ): List> { + TODO("Not yet implemented") + } +} + +class FollowCitizenRepository(requester: Requester) : FollowRepository(requester) { + override fun findByCitizen( + citizenId: UUID, + page: Int, + limit: Int + ): Paginated> { + return requester.run { + getFunction("find_follows_citizen_by_citizen") + .select( + page, + limit, + "created_by_id" to citizenId + ) + } + } + + override fun findFollowsByTarget( + target: Entity, + lastId: UUID?, + limit: Int + ): List> { TODO("Not yet implemented") } } diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/article/GetFollowArticle.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/article/GetFollowArticle.kt index 2b24f80..b2e2bed 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/routes/article/GetFollowArticle.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/article/GetFollowArticle.kt @@ -7,6 +7,7 @@ import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.database.FollowArticleRepository +import fr.dcproject.component.follow.routes.citizen.toOutput import io.ktor.application.call import io.ktor.http.HttpStatusCode import io.ktor.locations.KtorExperimentalLocationsAPI @@ -14,7 +15,6 @@ import io.ktor.locations.Location import io.ktor.locations.get import io.ktor.response.respond import io.ktor.routing.Route -import org.joda.time.DateTime import java.util.UUID @KtorExperimentalLocationsAPI @@ -30,19 +30,7 @@ object GetFollowArticle { ac.assert { canView(follow, citizenOrNull) } call.respond( HttpStatusCode.OK, - follow.let { f -> - object { - val id: UUID = f.id - val createdBy: Any = f.createdBy.toOutput() - val target: Any = f.target.let { t -> - object { - val id: UUID = t.id - val reference: String = f.target.reference - } - } - val createdAt: DateTime = f.createdAt - } - } + follow.toOutput() ) } ?: call.respond(HttpStatusCode.NoContent) } diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/FollowCitizen.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/FollowCitizen.kt new file mode 100644 index 0000000..a84aa3b --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/FollowCitizen.kt @@ -0,0 +1,36 @@ +package fr.dcproject.component.follow.routes.citizen + +import fr.dcproject.common.security.assert +import fr.dcproject.component.auth.citizen +import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth +import fr.dcproject.component.citizen.database.CitizenRef +import fr.dcproject.component.follow.FollowAccessControl +import fr.dcproject.component.follow.database.FollowCitizenRepository +import fr.dcproject.component.follow.database.FollowForUpdate +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.locations.Location +import io.ktor.locations.post +import io.ktor.response.respond +import io.ktor.routing.Route +import java.util.UUID + +@KtorExperimentalLocationsAPI +object FollowCitizen { + @Location("/citizens/{citizen}/follows") + class CitizenFollowRequest(citizen: UUID) { + val citizen = CitizenRef(citizen) + } + + fun Route.followCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) { + post { + mustBeAuth() + val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen) + ac.assert { canCreate(follow, citizenOrNull) } + repo.follow(follow) + call.respond(HttpStatusCode.Created) + } + } +} diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/GetFollowCitizen.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/GetFollowCitizen.kt new file mode 100644 index 0000000..b41a18d --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/GetFollowCitizen.kt @@ -0,0 +1,37 @@ +package fr.dcproject.component.follow.routes.citizen + +import fr.dcproject.common.response.toOutput +import fr.dcproject.common.security.assert +import fr.dcproject.component.auth.citizen +import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.citizen.database.CitizenRef +import fr.dcproject.component.follow.FollowAccessControl +import fr.dcproject.component.follow.database.FollowCitizenRepository +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.locations.Location +import io.ktor.locations.get +import io.ktor.response.respond +import io.ktor.routing.Route +import java.util.UUID + +@KtorExperimentalLocationsAPI +object GetFollowCitizen { + @Location("/citizens/{citizen}/follows") + class CitizenFollowRequest(citizen: UUID) { + val citizen = CitizenRef(citizen) + } + + fun Route.getFollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) { + get { + repo.findFollow(citizen, it.citizen)?.let { follow -> + ac.assert { canView(follow, citizenOrNull) } + call.respond( + HttpStatusCode.OK, + follow.toOutput() + ) + } ?: call.respond(HttpStatusCode.NoContent) + } + } +} diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/GetMyFollowsCitizen.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/GetMyFollowsCitizen.kt new file mode 100644 index 0000000..24c35ef --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/GetMyFollowsCitizen.kt @@ -0,0 +1,50 @@ +package fr.dcproject.component.follow.routes.citizen + +import fr.dcproject.common.response.toOutput +import fr.dcproject.common.security.assert +import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth +import fr.dcproject.component.citizen.database.CitizenRef +import fr.dcproject.component.follow.FollowAccessControl +import fr.dcproject.component.follow.database.FollowCitizenRepository +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.locations.Location +import io.ktor.locations.get +import io.ktor.response.respond +import io.ktor.routing.Route +import org.joda.time.DateTime +import java.util.UUID + +@KtorExperimentalLocationsAPI +object GetMyFollowsCitizen { + @Location("/citizens/{citizen}/follows/citizens") + class CitizenFollowCitizenRequest(citizen: UUID) { + val citizen = CitizenRef(citizen) + } + + fun Route.getMyFollowsCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) { + get { + mustBeAuth() + val follows = repo.findByCitizen(it.citizen) + ac.assert { canView(follows.result, citizenOrNull) } + call.respond( + HttpStatusCode.OK, + follows.toOutput { f -> + object { + val id: UUID = f.id + val createdBy: Any = f.createdBy.toOutput() + val target: Any = f.target.let { t -> + object { + val id: UUID = t.id + val reference: String = f.target.reference + } + } + val createdAt: DateTime = f.createdAt + } + } + ) + } + } +} diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/UnfollowCitizen.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/UnfollowCitizen.kt new file mode 100644 index 0000000..8b47903 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/UnfollowCitizen.kt @@ -0,0 +1,36 @@ +package fr.dcproject.component.follow.routes.citizen + +import fr.dcproject.common.security.assert +import fr.dcproject.component.auth.citizen +import fr.dcproject.component.auth.citizenOrNull +import fr.dcproject.component.auth.mustBeAuth +import fr.dcproject.component.citizen.database.CitizenRef +import fr.dcproject.component.follow.FollowAccessControl +import fr.dcproject.component.follow.database.FollowCitizenRepository +import fr.dcproject.component.follow.database.FollowForUpdate +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.locations.Location +import io.ktor.locations.delete +import io.ktor.response.respond +import io.ktor.routing.Route +import java.util.UUID + +@KtorExperimentalLocationsAPI +object UnfollowCitizen { + @Location("/citizens/{citizen}/follows") + class CitizenFollowRequest(citizen: UUID) { + val citizen = CitizenRef(citizen) + } + + fun Route.unfollowCitizen(repo: FollowCitizenRepository, ac: FollowAccessControl) { + delete { + mustBeAuth() + val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen) + ac.assert { canDelete(follow, citizenOrNull) } + repo.unfollow(follow) + call.respond(HttpStatusCode.NoContent) + } + } +} diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/install.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/install.kt new file mode 100644 index 0000000..046dfd2 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/install.kt @@ -0,0 +1,20 @@ +package fr.dcproject.component.follow.routes.citizen + +import fr.dcproject.component.follow.routes.citizen.FollowCitizen.followCitizen +import fr.dcproject.component.follow.routes.citizen.GetFollowCitizen.getFollowCitizen +import fr.dcproject.component.follow.routes.citizen.GetMyFollowsCitizen.getMyFollowsCitizen +import fr.dcproject.component.follow.routes.citizen.UnfollowCitizen.unfollowCitizen +import io.ktor.auth.authenticate +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.routing.Routing +import org.koin.ktor.ext.get + +@KtorExperimentalLocationsAPI +fun Routing.installFollowCitizenRoutes() { + authenticate(optional = true) { + followCitizen(get(), get()) + unfollowCitizen(get(), get()) + getFollowCitizen(get(), get()) + getMyFollowsCitizen(get(), get()) + } +} diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/response.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/response.kt new file mode 100644 index 0000000..87e72f1 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/citizen/response.kt @@ -0,0 +1,20 @@ +package fr.dcproject.component.follow.routes.citizen + +import fr.dcproject.common.response.toOutput +import fr.dcproject.component.follow.database.FollowForView +import org.joda.time.DateTime +import java.util.UUID + +fun FollowForView<*>.toOutput(): Any = this.let { f -> + object { + val id: UUID = f.id + val createdBy: Any = f.createdBy.toOutput() + val target: Any = f.target.let { t -> + object { + val id: UUID = t.id + val reference: String = f.target.reference + } + } + val createdAt: DateTime = f.createdAt + } +} diff --git a/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/GetFollowConstitution.kt b/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/GetFollowConstitution.kt index a2d7568..3908e5d 100644 --- a/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/GetFollowConstitution.kt +++ b/src/main/kotlin/fr/dcproject/component/follow/routes/constitution/GetFollowConstitution.kt @@ -7,6 +7,7 @@ import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.follow.FollowAccessControl import fr.dcproject.component.follow.database.FollowConstitutionRepository +import fr.dcproject.component.follow.routes.citizen.toOutput import io.ktor.application.call import io.ktor.http.HttpStatusCode import io.ktor.locations.KtorExperimentalLocationsAPI @@ -14,7 +15,6 @@ import io.ktor.locations.Location import io.ktor.locations.get import io.ktor.response.respond import io.ktor.routing.Route -import org.joda.time.DateTime import java.util.UUID @KtorExperimentalLocationsAPI @@ -30,19 +30,7 @@ object GetFollowConstitution { ac.assert { canView(follow, citizenOrNull) } call.respond( HttpStatusCode.OK, - follow.let { f -> - object { - val id: UUID = f.id - val createdBy: Any = f.createdBy.toOutput() - val target: Any = f.target.let { t -> - object { - val id: UUID = t.id - val reference: String = f.target.reference - } - } - val createdAt: DateTime = f.createdAt - } - } + follow.toOutput() ) } ?: call.respond(HttpStatusCode.NotFound) } diff --git a/src/main/kotlin/fr/dcproject/component/notification/NotificationConsumer.kt b/src/main/kotlin/fr/dcproject/component/notification/NotificationConsumer.kt deleted file mode 100644 index 8015f45..0000000 --- a/src/main/kotlin/fr/dcproject/component/notification/NotificationConsumer.kt +++ /dev/null @@ -1,112 +0,0 @@ -package fr.dcproject.component.notification - -import com.rabbitmq.client.AMQP.BasicProperties -import com.rabbitmq.client.BuiltinExchangeType.DIRECT -import com.rabbitmq.client.ConnectionFactory -import com.rabbitmq.client.Consumer -import com.rabbitmq.client.DefaultConsumer -import com.rabbitmq.client.Envelope -import fr.dcproject.common.entity.TargetRef -import fr.dcproject.component.follow.database.FollowArticleRepository -import fr.dcproject.component.follow.database.FollowConstitutionRepository -import fr.dcproject.component.follow.database.FollowForView -import io.ktor.utils.io.errors.IOException -import io.lettuce.core.RedisClient -import io.lettuce.core.api.async.RedisAsyncCommands -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.runBlocking -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -class NotificationConsumer( - private val rabbitFactory: ConnectionFactory, - private val redisClient: RedisClient, - private val followConstitutionRepo: FollowConstitutionRepository, - private val followArticleRepo: FollowArticleRepository, - private val notificationEmailSender: NotificationEmailSender, - private val exchangeName: String, -) { - private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis") - private val redis: RedisAsyncCommands = redisConnection.async() ?: error("Unable to connect to redis") - private val rabbitConnection = rabbitFactory.newConnection() - private val rabbitChannel = rabbitConnection.createChannel() - private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName) - - fun close() { - rabbitChannel.close() - rabbitConnection.close() - } - - fun start() { - /* Config Rabbit */ - rabbitFactory.newConnection().use { connection -> - connection.createChannel().use { channel -> - channel.queueDeclare("push", true, false, false, null) - channel.queueDeclare("email", true, false, false, null) - channel.exchangeDeclare(exchangeName, DIRECT, true) - channel.queueBind("push", exchangeName, "") - channel.queueBind("email", exchangeName, "") - } - } - - /* Define Consumer */ - val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) { - @Throws(IOException::class) - override fun handleDelivery( - consumerTag: String, - envelope: Envelope, - properties: BasicProperties, - body: ByteArray - ) = runBlocking { - followersFromMessage(body) { - redis.zadd( - "notification:${it.follow.createdBy.id}", - it.event.id, - it.rawMessage - ) - } - - rabbitChannel.basicAck(envelope.deliveryTag, false) - } - } - - val consumerEmail: Consumer = object : DefaultConsumer(rabbitChannel) { - @Throws(IOException::class) - override fun handleDelivery( - consumerTag: String, - envelope: Envelope, - properties: BasicProperties, - body: ByteArray - ) { - runBlocking { - followersFromMessage(body) { - notificationEmailSender.sendEmail(it.follow) - logger.debug("EmailSend to: ${it.follow.createdBy.id}") - } - } - rabbitChannel.basicAck(envelope.deliveryTag, false) - } - } - /* Launch Consumer */ - rabbitChannel.basicConsume("push", false, consumerPush) // The front consume the redis via Websocket - rabbitChannel.basicConsume("email", false, consumerEmail) - } - - private suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage) -> Unit) { - val rawMessage: String = body.toString(Charsets.UTF_8) - val notification: EntityNotification = Notification.fromString(rawMessage) - val follows = when (notification.type) { - "article" -> followArticleRepo.findFollowsByTarget(notification.target) - "constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target) - else -> error("event '${notification.type}' not implemented") - } - - follows.collect { action(DecodedMessage(notification, rawMessage, it)) } - } - - private class DecodedMessage( - val event: EntityNotification, - val rawMessage: String, - val follow: FollowForView - ) -} diff --git a/src/main/kotlin/fr/dcproject/component/notification/NotificationConsumerAbstract.kt b/src/main/kotlin/fr/dcproject/component/notification/NotificationConsumerAbstract.kt new file mode 100644 index 0000000..0b3f1e4 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/notification/NotificationConsumerAbstract.kt @@ -0,0 +1,67 @@ +package fr.dcproject.component.notification + +import com.rabbitmq.client.BuiltinExchangeType.DIRECT +import com.rabbitmq.client.ConnectionFactory +import com.rabbitmq.client.DefaultConsumer +import fr.dcproject.common.entity.Entity +import fr.dcproject.common.entity.TargetRef +import fr.dcproject.common.utils.consumeQueue +import fr.dcproject.component.follow.database.FollowArticleRepository +import fr.dcproject.component.follow.database.FollowCitizenRepository +import fr.dcproject.component.follow.database.FollowConstitutionRepository +import fr.dcproject.component.follow.database.FollowForView +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.runBlocking + +abstract class NotificationConsumerAbstract( + private val rabbitFactory: ConnectionFactory, + private val followConstitutionRepo: FollowConstitutionRepository, + private val followArticleRepo: FollowArticleRepository, + private val followCitizenRepo: FollowCitizenRepository, +) { + private val rabbitConnection = rabbitFactory.newConnection() + private val rabbitChannel = rabbitConnection.createChannel() + + fun close() { + rabbitChannel.close() + rabbitConnection.close() + } + + fun declareQueue(queueName: String, exchangeName: String) { + rabbitFactory.newConnection().use { connection -> + connection.createChannel().use { channel -> + channel.queueDeclare(queueName, true, false, false, null) + channel.exchangeDeclare(exchangeName, DIRECT, true) + channel.queueBind(queueName, exchangeName, "") + } + } + } + + protected fun consumeQueue(queueName: String, callback: DefaultConsumer.(DecodedMessage<*>) -> Unit) = + rabbitChannel.consumeQueue(queueName) { body -> + runBlocking { + followersFromMessage(body) { + callback(it) + } + } + } + + protected suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage<*>) -> Unit) { + val rawMessage: String = body.toString(Charsets.UTF_8) + val notification: EntityNotificationMessage<*> = NotificationMessage.fromString(rawMessage) + val follows = when (notification.type) { + "article" -> followArticleRepo.findFollowsByTarget(notification.target) + "constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target) + "citizen" -> followCitizenRepo.findFollowsByTarget(notification.target) + else -> error("event '${notification.type}' not implemented") + } + + follows.collect { action(DecodedMessage(notification, rawMessage, it)) } + } + + protected class DecodedMessage ( + val event: EntityNotificationMessage, + val rawMessage: String, + val follow: FollowForView + ) +} diff --git a/src/main/kotlin/fr/dcproject/component/notification/NotificationEmailSender.kt b/src/main/kotlin/fr/dcproject/component/notification/NotificationEmailSender.kt deleted file mode 100644 index f693636..0000000 --- a/src/main/kotlin/fr/dcproject/component/notification/NotificationEmailSender.kt +++ /dev/null @@ -1,70 +0,0 @@ -package fr.dcproject.component.notification - -import com.sendgrid.helpers.mail.Mail -import com.sendgrid.helpers.mail.objects.Content -import com.sendgrid.helpers.mail.objects.Email -import fr.dcproject.common.email.Mailer -import fr.dcproject.common.entity.EntityI -import fr.dcproject.common.entity.TargetRef -import fr.dcproject.component.article.database.ArticleRepository -import fr.dcproject.component.article.database.ArticleWithTitleI -import fr.dcproject.component.citizen.database.CitizenCreatorI -import fr.dcproject.component.citizen.database.CitizenRepository -import fr.dcproject.component.follow.database.FollowForView -import java.util.UUID - -class NotificationEmailSender( - private val mailer: Mailer, - private val domain: String, - private val citizenRepo: CitizenRepository, - private val articleRepo: ArticleRepository -) { - fun sendEmail(follow: FollowForView) { - val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id) - val target = when (follow.target.reference) { - "article" -> - articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id) - else -> noTarget(follow.target.id) - } - val subject = when (follow.target.reference) { - "article" -> """New version for article "${target.title}"""" - else -> "Notification" - } - mailer.sendEmail { - Mail( - Email("notification@$domain"), - subject, - Email(citizen.email), - Content("text/plain", generateContent(citizen, target)) - ).apply { - addContent(Content("text/html", generateHtmlContent(citizen, target))) - } - } - } - - private fun generateHtmlContent(citizen: CitizenCreatorI, target: EntityI): String? { - return when (target) { - is ArticleWithTitleI -> """ - Hello ${citizen.name.getFullName()},
- The article "${target.title}" was updated, check it here - """.trimIndent() - else -> noTarget(target.id) - } - } - - private fun generateContent(citizen: CitizenCreatorI, target: EntityI): String { - return when (target) { - is ArticleWithTitleI -> """ - Hello ${citizen.name.getFullName()}, - The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id} - """.trimIndent() - else -> noTarget(target.id) - } - } - - class NoCitizen(message: String) : Exception(message) - class NoTarget(message: String) : Exception(message) - - private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id") - private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id") -} diff --git a/src/main/kotlin/fr/dcproject/component/notification/Notification.kt b/src/main/kotlin/fr/dcproject/component/notification/NotificationMessage.kt similarity index 81% rename from src/main/kotlin/fr/dcproject/component/notification/Notification.kt rename to src/main/kotlin/fr/dcproject/component/notification/NotificationMessage.kt index 57e4320..0b7c985 100644 --- a/src/main/kotlin/fr/dcproject/component/notification/Notification.kt +++ b/src/main/kotlin/fr/dcproject/component/notification/NotificationMessage.kt @@ -16,9 +16,9 @@ import java.util.concurrent.atomic.AtomicInteger @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) @JsonSubTypes( - JsonSubTypes.Type(value = ArticleUpdateNotification::class, name = "article") + JsonSubTypes.Type(value = ArticleUpdateNotificationMessage::class, name = "article") ) -open class Notification( +open class NotificationMessage( val type: String, val createdAt: DateTime = DateTime.now() ) { @@ -48,16 +48,16 @@ open class Notification( configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } - inline fun fromString(raw: String): T = mapper.readValue(raw) + inline fun fromString(raw: String): T = mapper.readValue(raw) } } -open class EntityNotification( - val target: Entity, +open class EntityNotificationMessage ( + val target: E, type: String, val action: String -) : Notification(type) +) : NotificationMessage(type) -class ArticleUpdateNotification( +class ArticleUpdateNotificationMessage( target: ArticleForView -) : EntityNotification(target, "article", "update") +) : EntityNotificationMessage(target, "article", "update") diff --git a/src/main/kotlin/fr/dcproject/component/notification/Publisher.kt b/src/main/kotlin/fr/dcproject/component/notification/NotificationPublisherAsync.kt similarity index 68% rename from src/main/kotlin/fr/dcproject/component/notification/Publisher.kt rename to src/main/kotlin/fr/dcproject/component/notification/NotificationPublisherAsync.kt index d10d34f..2944926 100644 --- a/src/main/kotlin/fr/dcproject/component/notification/Publisher.kt +++ b/src/main/kotlin/fr/dcproject/component/notification/NotificationPublisherAsync.kt @@ -7,12 +7,15 @@ import kotlinx.coroutines.coroutineScope import org.slf4j.Logger import org.slf4j.LoggerFactory -class Publisher( +class NotificationPublisherAsync( private val factory: ConnectionFactory, - private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName), + private val logger: Logger = LoggerFactory.getLogger(NotificationPublisherAsync::class.qualifiedName), private val exchangeName: String, ) { - suspend fun publish(it: T): Deferred = coroutineScope { + /** + * Publish a new notification message to RabbitMQ + */ + suspend fun > publishAsync(it: T): Deferred = coroutineScope { async { factory.newConnection().use { connection -> connection.createChannel().use { channel -> diff --git a/src/main/kotlin/fr/dcproject/component/notification/email/NotificationEmailConsumer.kt b/src/main/kotlin/fr/dcproject/component/notification/email/NotificationEmailConsumer.kt new file mode 100644 index 0000000..bbb2cc0 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/notification/email/NotificationEmailConsumer.kt @@ -0,0 +1,33 @@ +package fr.dcproject.component.notification.email + +import com.rabbitmq.client.ConnectionFactory +import fr.dcproject.component.follow.database.FollowArticleRepository +import fr.dcproject.component.follow.database.FollowCitizenRepository +import fr.dcproject.component.follow.database.FollowConstitutionRepository +import fr.dcproject.component.notification.NotificationConsumerAbstract +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class NotificationEmailConsumer( + rabbitFactory: ConnectionFactory, + followConstitutionRepo: FollowConstitutionRepository, + followArticleRepo: FollowArticleRepository, + followCitizenRepo: FollowCitizenRepository, + private val notificationEmailSender: NotificationEmailSender, + private val exchangeName: String, +) : NotificationConsumerAbstract(rabbitFactory, followConstitutionRepo, followArticleRepo, followCitizenRepo) { + private val logger: Logger = LoggerFactory.getLogger(NotificationEmailConsumer::class.qualifiedName) + + fun start() { + /* Config Rabbit */ + declareQueue(QUEUE_NAME, exchangeName) + consumeQueue(QUEUE_NAME) { message -> + notificationEmailSender.sendEmail(message.follow) + logger.debug("EmailSend to: ${message.follow.createdBy.id}") + } + } + + companion object { + private const val QUEUE_NAME = "email" + } +} diff --git a/src/main/kotlin/fr/dcproject/component/notification/email/NotificationEmailSender.kt b/src/main/kotlin/fr/dcproject/component/notification/email/NotificationEmailSender.kt new file mode 100644 index 0000000..fb68ac2 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/notification/email/NotificationEmailSender.kt @@ -0,0 +1,70 @@ +package fr.dcproject.component.notification.email + +import com.sendgrid.helpers.mail.Mail +import com.sendgrid.helpers.mail.objects.Content +import com.sendgrid.helpers.mail.objects.Email +import fr.dcproject.common.email.Mailer +import fr.dcproject.common.entity.TargetRef +import fr.dcproject.component.article.database.ArticleForView +import fr.dcproject.component.article.database.ArticleRepository +import fr.dcproject.component.citizen.database.Citizen +import fr.dcproject.component.citizen.database.CitizenRepository +import fr.dcproject.component.follow.database.FollowForView +import fr.dcproject.component.notification.email.content.ArticleNotificationEmailContent +import fr.dcproject.component.notification.email.content.CitizenNotificationEmailContent +import java.util.UUID + +/** + * Send notification email on the follower + */ +class NotificationEmailSender( + private val mailer: Mailer, + private val domain: String, + private val citizenRepo: CitizenRepository, + private val articleRepo: ArticleRepository +) { + /** + * Send the Notification Email to the follower user + */ + fun sendEmail(follow: FollowForView) { + val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id) + + /** + * Find the complete target entity by its ID according to its reference + */ + val target = when (follow.target.reference) { + "article" -> articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id) + "citizen" -> citizenRepo.findById(follow.target.id) ?: noTarget(follow.target.id) + else -> noTarget(follow.target.id) + } + + /** + * Find content of the email according to the target type + */ + val content = when (target) { + is ArticleForView -> ArticleNotificationEmailContent(citizen, target, domain) + is Citizen -> CitizenNotificationEmailContent(citizen, target, domain) + else -> noTargetTypeImplementation(follow.target.reference) + } + + /* Send email */ + mailer.sendEmail { + Mail( + Email("notification@$domain"), + content.subject, + Email(citizen.email), + Content("text/plain", content.content) + ).apply { + addContent(Content("text/html", content.contentHtml)) + } + } + } + + class NoCitizen(message: String) : Exception(message) + class NoTarget(message: String) : Exception(message) + class NoTargetTypeImplement(message: String) : Exception(message) + + private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id") + private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id") + private fun noTargetTypeImplementation(type: String): Nothing = throw NoTargetTypeImplement("No Target type implemented: $type") +} diff --git a/src/main/kotlin/fr/dcproject/component/notification/email/content/ArticleNotificationEmailContent.kt b/src/main/kotlin/fr/dcproject/component/notification/email/content/ArticleNotificationEmailContent.kt new file mode 100644 index 0000000..2d634ca --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/notification/email/content/ArticleNotificationEmailContent.kt @@ -0,0 +1,29 @@ +package fr.dcproject.component.notification.email.content + +import fr.dcproject.component.article.database.ArticleWithTitleI +import fr.dcproject.component.citizen.database.Citizen + +class ArticleNotificationEmailContent( + private val citizen: Citizen, + private val target: ArticleWithTitleI, + private val domain: String, +) : NotificationEmailContent { + override val subject: String + get() = """New version for article "${target.title}"""" + + override val contentHtml + get() = run { + """ + Hello ${citizen.name.getFullName()},
+ The article "${target.title}" was updated, check it here + """.trimIndent() + } + + override val content + get() = run { + """ + Hello ${citizen.name.getFullName()}, + The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id} + """.trimIndent() + } +} diff --git a/src/main/kotlin/fr/dcproject/component/notification/email/content/CitizenNotificationEmailContent.kt b/src/main/kotlin/fr/dcproject/component/notification/email/content/CitizenNotificationEmailContent.kt new file mode 100644 index 0000000..af1a5dc --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/notification/email/content/CitizenNotificationEmailContent.kt @@ -0,0 +1,28 @@ +package fr.dcproject.component.notification.email.content + +import fr.dcproject.component.citizen.database.Citizen + +class CitizenNotificationEmailContent( + private val citizen: Citizen, + private val target: Citizen, + private val domain: String, +) : NotificationEmailContent { + override val subject: String + get() = """New activity for the citizen "${target.name}"""" + + override val contentHtml + get() = run { + """ + Hello ${citizen.name.getFullName()}, + The citizen "${target.name}" was new activity, check it here: here + """.trimIndent() + } + + override val content + get() = run { + """ + Hello ${citizen.name.getFullName()}, + The citizen "${target.name}" was new activity, check it here: http://$domain/citizens/${target.id} + """.trimIndent() + } +} diff --git a/src/main/kotlin/fr/dcproject/component/notification/email/content/NotificationEmailContent.kt b/src/main/kotlin/fr/dcproject/component/notification/email/content/NotificationEmailContent.kt new file mode 100644 index 0000000..bd2a3fe --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/notification/email/content/NotificationEmailContent.kt @@ -0,0 +1,7 @@ +package fr.dcproject.component.notification.email.content + +interface NotificationEmailContent { + val subject: String + val content: String + val contentHtml: String +} diff --git a/src/main/kotlin/fr/dcproject/component/notification/push/NotificationPushConsumer.kt b/src/main/kotlin/fr/dcproject/component/notification/push/NotificationPushConsumer.kt new file mode 100644 index 0000000..46768be --- /dev/null +++ b/src/main/kotlin/fr/dcproject/component/notification/push/NotificationPushConsumer.kt @@ -0,0 +1,41 @@ +package fr.dcproject.component.notification.push + +import com.rabbitmq.client.ConnectionFactory +import fr.dcproject.component.follow.database.FollowArticleRepository +import fr.dcproject.component.follow.database.FollowCitizenRepository +import fr.dcproject.component.follow.database.FollowConstitutionRepository +import fr.dcproject.component.notification.NotificationConsumerAbstract +import io.lettuce.core.RedisClient +import io.lettuce.core.api.async.RedisAsyncCommands +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class NotificationPushConsumer( + rabbitFactory: ConnectionFactory, + followConstitutionRepo: FollowConstitutionRepository, + followArticleRepo: FollowArticleRepository, + followCitizenRepo: FollowCitizenRepository, + redisClient: RedisClient, + private val exchangeName: String, +) : NotificationConsumerAbstract(rabbitFactory, followConstitutionRepo, followArticleRepo, followCitizenRepo) { + private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis") + private val redis: RedisAsyncCommands = redisConnection.async() ?: error("Unable to connect to redis") + private val logger: Logger = LoggerFactory.getLogger(NotificationPushConsumer::class.qualifiedName) + + fun start() { + /* Config Rabbit */ + declareQueue(QUEUE_NAME, exchangeName) + consumeQueue(QUEUE_NAME) { message -> + redis.zadd( + "notification:${message.follow.createdBy.id}", + message.event.id, + message.rawMessage + ) + logger.debug("Notification was transferred to the redis (follower: ${message.follow.createdBy.id})") + } + } + + companion object { + private const val QUEUE_NAME = "push" + } +} diff --git a/src/main/kotlin/fr/dcproject/component/notification/NotificationsPush.kt b/src/main/kotlin/fr/dcproject/component/notification/push/NotificationPushListener.kt similarity index 55% rename from src/main/kotlin/fr/dcproject/component/notification/NotificationsPush.kt rename to src/main/kotlin/fr/dcproject/component/notification/push/NotificationPushListener.kt index 445cca1..96b77b0 100644 --- a/src/main/kotlin/fr/dcproject/component/notification/NotificationsPush.kt +++ b/src/main/kotlin/fr/dcproject/component/notification/push/NotificationPushListener.kt @@ -1,8 +1,9 @@ -package fr.dcproject.component.notification +package fr.dcproject.component.notification.push import com.fasterxml.jackson.core.JsonProcessingException import fr.dcproject.component.auth.citizen import fr.dcproject.component.citizen.database.CitizenI +import fr.dcproject.component.notification.NotificationMessage import io.ktor.http.cio.websocket.Frame import io.ktor.http.cio.websocket.Frame.Text import io.ktor.http.cio.websocket.readText @@ -28,31 +29,42 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory -class NotificationsPush( +/** + * Listen a custom flow to mark as read a message. + * + * And listen the redis subscription flow and call a callback when a new message arrives + */ +class NotificationPushListener( private val redis: RedisAsyncCommands, private val redisConnectionPubSub: StatefulRedisPubSubConnection, citizen: CitizenI, - incoming: Flow, - onReceive: suspend (Notification) -> Unit, + incoming: Flow, + onReceive: suspend (NotificationMessage) -> Unit, ) { - class Builder(val redisClient: RedisClient) { - private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis") - private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis PubSub") - private val redis: RedisAsyncCommands = redisConnection.async() ?: error("Unable to connect to redis Async") + class Builder(redisClient: RedisClient) { + private val redisConnection = redisClient.connect() + private val redisConnectionPubSub = redisClient.connectPubSub() + private val redis: RedisAsyncCommands = redisConnection.async() + /** + * Build Listener with citizen, incoming flow and set an outgoing callback + */ fun build( citizen: CitizenI, - incoming: Flow, - onReceive: suspend (Notification) -> Unit, - ): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onReceive) + incoming: Flow, + onReceive: suspend (NotificationMessage) -> Unit, + ): NotificationPushListener = NotificationPushListener(redis, redisConnectionPubSub, citizen, incoming, onReceive) + /** + * Build NotificationPush with only a WebSocket session + */ @ExperimentalCoroutinesApi - fun build(ws: DefaultWebSocketServerSession): NotificationsPush { + fun build(ws: DefaultWebSocketServerSession): NotificationPushListener { /* Convert channel of string from websocket, to a flow of Notification object */ - val incomingFlow: Flow = ws.incoming.consumeAsFlow() - .mapNotNull { it as? Frame.Text } + val incomingFlow: Flow = ws.incoming.consumeAsFlow() + .mapNotNull { it as? Text } .map { it.readText() } - .map { Notification.fromString(it) } + .map { NotificationMessage.fromString(it) } return build(ws.call.citizen, incomingFlow) { ws.outgoing.send(Text(it.toString())) @@ -62,30 +74,42 @@ class NotificationsPush( } } + /** + * The key of the SortedSet in Redis which contains all the messages of a user + */ private val key = "notification:${citizen.id}" - private var score: Double = 0.0 + /** + * The last score (a kind of sorted ids) of message + */ + private var lastScore: Double = 0.0 + /** + * Configure the listener to listen all new notifications + */ private val listener = object : RedisPubSubAdapter() { /* On new key publish */ override fun message(pattern: String?, channel: String?, message: String?) { runBlocking { - getNotifications().collect { + getNewUnreadNotifications().collect { onReceive(it) } } } } + /** + * Init the listener and the callback + */ init { /* Mark as read all incoming notifications */ GlobalScope.launch { incoming.collect { - markAsRead(it) + it.markAsRead() } } /* Get old notification and sent it to websocket */ runBlocking { - getNotifications().collect { + getNewUnreadNotifications().collect { onReceive(it) } } @@ -95,38 +119,55 @@ class NotificationsPush( addListener(listener) /* Register to the events */ - async()?.psubscribe("__key*__:$key") ?: error("Unable to subscribe to redis events") + async()?.psubscribe("__key*__:$key") } } + /** + * Close the redis subscription + */ fun close() { redisConnectionPubSub.removeListener(listener) } - /* Return flow with all new notifications */ - private fun getNotifications() = flow { + /** + * Get All new notification from redis and + * Return flow with notifications + * + * On start, on the first call, this method return all unread notification of the user + * + * Internally this method return all messages that greater of the lastScore, + * then define the lastScore with the score of the last message. + */ + private fun getNewUnreadNotifications() = flow { redis .zrangebyscoreWithScores( key, Range.from( - Boundary.excluding(score), + Boundary.excluding(lastScore), Boundary.including(Double.POSITIVE_INFINITY) ), Limit.from(100) ) .get().forEach { - emit(Notification.fromString(it.value)) - if (it.score > score) score = it.score + /* Build message object from raw string and return it */ + emit(NotificationMessage.fromString(it.value)) + if (it.score > lastScore) lastScore = it.score } } - private suspend fun markAsRead(notificationMessage: Notification) = coroutineScope { + /** + * Mark one notification as read. + * + * Internally, this method remove the message of the SortedSet in redis + */ + private suspend fun NotificationMessage.markAsRead() = coroutineScope { try { redis.zremrangebyscore( key, Range.from( - Boundary.including(notificationMessage.id), - Boundary.including(notificationMessage.id) + Boundary.including(id), + Boundary.including(id) ) ) } catch (e: JsonProcessingException) { diff --git a/src/main/kotlin/fr/dcproject/component/notification/routes/Notification.kt b/src/main/kotlin/fr/dcproject/component/notification/routes/Notification.kt index 58669ad..1a3bbfc 100644 --- a/src/main/kotlin/fr/dcproject/component/notification/routes/Notification.kt +++ b/src/main/kotlin/fr/dcproject/component/notification/routes/Notification.kt @@ -1,6 +1,6 @@ package fr.dcproject.component.notification.routes -import fr.dcproject.component.notification.NotificationsPush +import fr.dcproject.component.notification.push.NotificationPushListener import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.routing.Route import io.ktor.websocket.webSocket @@ -13,8 +13,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi */ @ExperimentalCoroutinesApi @KtorExperimentalLocationsAPI -fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) { +fun Route.notificationArticle(pushListenerBuilder: NotificationPushListener.Builder) { webSocket("/notifications") { - pushBuilder.build(this) + pushListenerBuilder.build(this) } } diff --git a/src/main/resources/openapi.yaml b/src/main/resources/openapi.yaml index 3f3989c..e42ad10 100644 --- a/src/main/resources/openapi.yaml +++ b/src/main/resources/openapi.yaml @@ -955,13 +955,105 @@ paths: description: Return only http status 204 on success 401: $ref: '#/components/responses/401' + + /citizens/{citizen}/follows: + parameters: + - $ref: '#/components/parameters/citizen' + get: + security: + - JWTAuth: [ ] + summary: Return Follows of citizen + tags: + - follow + - citizen + responses: + 200: + description: Return follows + content: + application/json: + schema: + $ref: '#/components/schemas/FollowResponse' + 404: + description: Citizen not exist + content: + application/json: + schema: + $ref: '#/components/schemas/404' + post: + security: + - JWTAuth: [] + summary: Follow citizen + description: Follow a citizen to receive notifications of his activity + tags: + - follow + - citizen + responses: + 201: + description: Return only http status 201 on success + 401: + $ref: '#/components/responses/401' + 404: + description: Citizen not exist + content: + application/json: + schema: + $ref: '#/components/schemas/404' + delete: + security: + - JWTAuth: [ ] + summary: Unfollow one citizen + tags: + - follow + - citizen + responses: + 204: + description: Return only http status 204 on success + 401: + $ref: '#/components/responses/401' + 404: + description: Citizen not exist + content: + application/json: + schema: + $ref: '#/components/schemas/404' + /citizens/{citizen}/follows/citizens: + parameters: + - $ref: '#/components/parameters/citizen' + get: + security: + - JWTAuth: [ ] + summary: Return citizen Follow of citizen + tags: + - follow + - citizen + responses: + 200: + description: Return citizen Follow of citizen + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Paginated' + - type: object + properties: + result: + type: array + items: + $ref: '#/components/schemas/FollowResponse' + 404: + description: Citizen not exist + content: + application/json: + schema: + $ref: '#/components/schemas/404' + /citizens/{citizen}/follows/articles: parameters: - $ref: '#/components/parameters/citizen' get: security: - JWTAuth: [ ] - summary: Return Follow or nothing if you not follow + summary: Return article Follow of citizen tags: - follow - article @@ -1036,7 +1128,7 @@ paths: - citizen responses: 200: - description: Return your follows + description: Return constitution Follow of citizen content: application/json: schema: diff --git a/src/main/resources/sql/functions/follow/find_follows_article_by_target.sql b/src/main/resources/sql/functions/follow/find_follows_article_by_target.sql index 6baded1..eae0937 100644 --- a/src/main/resources/sql/functions/follow/find_follows_article_by_target.sql +++ b/src/main/resources/sql/functions/follow/find_follows_article_by_target.sql @@ -1,20 +1,21 @@ create or replace function find_follows_article_by_target( _target_id uuid, - "limit" int default 50, - "offset" int default 0, - out resource json, - out total int + _limit int default 50, + _start_id uuid default null, + out resource json ) language plpgsql as $$ declare _version_id uuid = (select version_id from article where id = _target_id); + _start_at timestamp default '2000-01-01 00:00:00'::timestamp; + _article_creator_id uuid = (select created_by_id from article where id = _target_id); begin - select json_agg(t), ( - select count(f.id) - from follow f - join article a on f.target_id = a.id - where a.version_id = _version_id) - into resource, total + if _start_id is not null then + select created_at into _start_at from follow where id = _start_id; + end if; + + select json_agg(t) + into resource from ( select f.id, @@ -22,11 +23,17 @@ begin f.target_reference, json_build_object('id', f.target_id) as target, find_citizen_by_id_with_user(f.created_by_id) as created_by - from follow_article as f - join article a on f.target_id = a.id - where a.version_id = _version_id + from follow as f + left join article a on f.target_reference = 'article'::regclass and f.target_id = a.id + where ( + (f.target_reference = 'article'::regclass and a.version_id = _version_id) + or + (f.target_reference = 'citizen'::regclass and f.target_id = _article_creator_id) + ) + and f.created_at >= _start_at + and (_start_id is null or f.id != _start_id) order by f.created_at - limit "limit" offset "offset" + limit _limit ) as t; end $$; diff --git a/src/main/resources/sql/functions/follow/find_follows_citizen_by_citizen.sql b/src/main/resources/sql/functions/follow/find_follows_citizen_by_citizen.sql new file mode 100644 index 0000000..82fee60 --- /dev/null +++ b/src/main/resources/sql/functions/follow/find_follows_citizen_by_citizen.sql @@ -0,0 +1,24 @@ +create or replace function find_follows_citizen_by_citizen( + _created_by_id uuid, + "limit" int default 50, + "offset" int default 0, + out resource json, + out total int +) language plpgsql as +$$ +begin + select json_agg(t), (select count(id) from follow) + into resource, total + from ( + select + f.*, + find_citizen_by_id_with_user(f.target_id) as target, + find_citizen_by_id_with_user(f.created_by_id) as created_by + from follow as f + where created_by_id = _created_by_id + order by created_at desc, + f.created_at desc + limit "limit" offset "offset" + ) as t; +end; +$$; diff --git a/src/test/kotlin/functional/NotificationConsumerTest.kt b/src/test/kotlin/functional/NotificationConsumerTest.kt index 4d35704..2a44944 100644 --- a/src/test/kotlin/functional/NotificationConsumerTest.kt +++ b/src/test/kotlin/functional/NotificationConsumerTest.kt @@ -10,10 +10,11 @@ import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.follow.database.FollowArticleRepository import fr.dcproject.component.follow.database.FollowForView -import fr.dcproject.component.notification.ArticleUpdateNotification -import fr.dcproject.component.notification.NotificationConsumer -import fr.dcproject.component.notification.NotificationEmailSender -import fr.dcproject.component.notification.Publisher +import fr.dcproject.component.notification.ArticleUpdateNotificationMessage +import fr.dcproject.component.notification.NotificationPublisherAsync +import fr.dcproject.component.notification.email.NotificationEmailConsumer +import fr.dcproject.component.notification.email.NotificationEmailSender +import fr.dcproject.component.notification.push.NotificationPushConsumer import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.util.KtorExperimentalAPI import io.lettuce.core.RedisClient @@ -65,7 +66,7 @@ class NotificationConsumerTest { @KtorExperimentalAPI @ExperimentalCoroutinesApi @Test - fun `can be send notification`() = runBlocking { + fun `can be receive article update notification when follow article`() = runBlocking { val config: Configuration = Configuration("application-test.conf") /* Create mocks and spy's */ val emailSender = mockk() { @@ -88,21 +89,30 @@ class NotificationConsumerTest { } /* Config consumer */ - val consumer = NotificationConsumer( + val emailConsumer = NotificationEmailConsumer( rabbitFactory = rabbitFactory, - redisClient = redisClient, followArticleRepo = followArticleRepo, - followConstitutionRepo = mockk(), + followConstitutionRepo = mockk(), // TODO test followConstitution + followCitizenRepo = mockk(), // TODO test followCitizen notificationEmailSender = emailSender, exchangeName = "notification", ).apply { start() } + val pushConsumer = NotificationPushConsumer( + rabbitFactory = rabbitFactory, + followArticleRepo = followArticleRepo, + followConstitutionRepo = mockk(), // TODO test followConstitution + followCitizenRepo = mockk(), // TODO test followCitizen + redisClient = redisClient, + exchangeName = "notification", + ).apply { start() } + /* Push message */ - Publisher( + NotificationPublisherAsync( factory = rabbitFactory, exchangeName = "notification", - ).publish( - ArticleUpdateNotification( + ).publishAsync( + ArticleUpdateNotificationMessage( ArticleForView( title = "MyTitle", content = "myContent", @@ -121,6 +131,7 @@ class NotificationConsumerTest { verify(timeout = 2000) { emailSender.sendEmail(any()) } verify(timeout = 2000) { asyncCommand.zadd(any(), any(), any()) } - consumer.close() + emailConsumer.close() + pushConsumer.close() } } diff --git a/src/test/kotlin/functional/NotificationsPushTest.kt b/src/test/kotlin/functional/NotificationsPushTest.kt index 7464c6b..425adfb 100644 --- a/src/test/kotlin/functional/NotificationsPushTest.kt +++ b/src/test/kotlin/functional/NotificationsPushTest.kt @@ -6,9 +6,9 @@ import fr.dcproject.component.article.database.ArticleForView import fr.dcproject.component.auth.database.UserCreator import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenI -import fr.dcproject.component.notification.ArticleUpdateNotification -import fr.dcproject.component.notification.Notification -import fr.dcproject.component.notification.NotificationsPush +import fr.dcproject.component.notification.ArticleUpdateNotificationMessage +import fr.dcproject.component.notification.NotificationMessage +import fr.dcproject.component.notification.push.NotificationPushListener import io.lettuce.core.RedisClient import io.mockk.every import io.mockk.spyk @@ -68,14 +68,14 @@ internal class NotificationsPushTest { title = "Super Title", ) /* Init two notification, one called before subscription, and the other after */ - val notifBeforeSubscribe = ArticleUpdateNotification(article) + val notifBeforeSubscribe = ArticleUpdateNotificationMessage(article) runBlocking { delay(100) } - val notifAfterSubscribe = ArticleUpdateNotification(article) + val notifAfterSubscribe = ArticleUpdateNotificationMessage(article) /* init event for emulate incoming message from websocket */ - val event = MutableSharedFlow() + val event = MutableSharedFlow() val incomingFlow = event.asSharedFlow() spyk(object { var counter = 0 }).run { /* Counter for count the callback of notification */ @@ -90,7 +90,7 @@ internal class NotificationsPushTest { } /* Init NotificationPush system, and set assertion in callback */ - val notificationPush = NotificationsPush.Builder(redisClient).build(citizen, incomingFlow) { + val notificationPush = NotificationPushListener.Builder(redisClient).build(citizen, incomingFlow) { counter++ if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id else it.id `should be equal to` notifAfterSubscribe.id diff --git a/src/test/kotlin/integration/Follow citizen routes.kt b/src/test/kotlin/integration/Follow citizen routes.kt new file mode 100644 index 0000000..108a684 --- /dev/null +++ b/src/test/kotlin/integration/Follow citizen routes.kt @@ -0,0 +1,96 @@ +package integration + +import integration.steps.`when`.`When I send a DELETE request` +import integration.steps.`when`.`When I send a GET request` +import integration.steps.`when`.`When I send a POST request` +import integration.steps.given.`And follow citizen` +import integration.steps.given.`Given I have citizen` +import integration.steps.given.`authenticated as` +import integration.steps.given.`with no content` +import integration.steps.then.`And the response should be null` +import integration.steps.then.`And the response should contain` +import integration.steps.then.`And the response should not be null` +import integration.steps.then.`Then the response should be` +import integration.steps.then.and +import io.ktor.http.HttpStatusCode.Companion.Created +import io.ktor.http.HttpStatusCode.Companion.NoContent +import io.ktor.http.HttpStatusCode.Companion.OK +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tags(Tag("integration"), Tag("article"), Tag("follow")) +class `Follow citizen routes` : BaseTest() { + @Test + fun `I can follow citizen`() { + withIntegrationApplication { + /* Followed user */ + `Given I have citizen`("John", "Glenn", id = "7e1580c5-05b7-4557-84f4-faac9f0a9441") + /* Current user */ + `Given I have citizen`("Valentina", "Terechkova") + `When I send a POST request`("/citizens/7e1580c5-05b7-4557-84f4-faac9f0a9441/follows") { + `authenticated as`("Valentina", "Terechkova") + `with no content`() + } `Then the response should be` Created + } + } + + @Test + fun `I can get my follow citizen`() { + withIntegrationApplication { + /* Followed user */ + `Given I have citizen`("Jean-Loup", "Chrétien", id = "c2432b94-a509-4116-a8b6-9774bc963372") + /* Current user */ + `Given I have citizen`("John", "Young", id = "6d41ce65-9df7-47e0-af46-8da4a909490b") { + `And follow citizen`("c2432b94-a509-4116-a8b6-9774bc963372") + } + /* Get my all follows */ + `When I send a GET request`("/citizens/6d41ce65-9df7-47e0-af46-8da4a909490b/follows/citizens") { + `authenticated as`("John", "Young") + } `Then the response should be` OK and { + `And the response should not be null`() + `And the response should contain`("$.currentPage", 1) + `And the response should contain`("$.limit", 50) + } + } + } + + @Test + fun `I can unfollow citizen`() { + withIntegrationApplication { + /* Followed user */ + `Given I have citizen`("Bruce", "McCandless", id = "680c7af7-d2de-4249-bfcb-47007ef546fe") + /* Current user */ + `Given I have citizen`("Jean-François", "Clervoy", id = "a12455ae-1047-43ff-826d-0d826dbe90f7") { + `And follow citizen`("680c7af7-d2de-4249-bfcb-47007ef546fe") + } + `When I send a DELETE request`("/citizens/680c7af7-d2de-4249-bfcb-47007ef546fe/follows") { + `authenticated as`("Jean-François", "Clervoy") + `with no content`() + } `Then the response should be` NoContent and { + `And the response should be null`() + } + } + } + + @Test + fun `I can know if I follow an citizen`() { + withIntegrationApplication { + /* Followed user */ + `Given I have citizen`("Eugene", "Cernan", id = "c755788f-7f48-4cde-8ff0-e75bcffdafc2") + /* Current user */ + `Given I have citizen`("Buzz", "Aldrin", id = "39e2915a-e96f-43ea-babd-bd339d8bf197") { + `And follow citizen`("c755788f-7f48-4cde-8ff0-e75bcffdafc2") + } + `When I send a GET request`("/citizens/c755788f-7f48-4cde-8ff0-e75bcffdafc2/follows") { + `authenticated as`("Buzz", "Aldrin") + `with no content`() + } `Then the response should be` OK and { + `And the response should not be null`() + `And the response should contain`("$.target.id", "c755788f-7f48-4cde-8ff0-e75bcffdafc2") + } + } + } +} diff --git a/src/test/kotlin/integration/Notification routes.kt b/src/test/kotlin/integration/Notification routes.kt index d50b298..afff34f 100644 --- a/src/test/kotlin/integration/Notification routes.kt +++ b/src/test/kotlin/integration/Notification routes.kt @@ -1,25 +1,20 @@ package integration -import fr.dcproject.common.utils.toUUID -import fr.dcproject.component.article.database.ArticleForView -import fr.dcproject.component.auth.database.UserCreator -import fr.dcproject.component.citizen.database.CitizenCreator import fr.dcproject.component.citizen.database.CitizenI.Name -import fr.dcproject.component.notification.ArticleUpdateNotification -import fr.dcproject.component.notification.Notification -import fr.dcproject.component.notification.Publisher +import fr.dcproject.component.notification.ArticleUpdateNotificationMessage +import fr.dcproject.component.notification.NotificationMessage +import integration.steps.given.`And follow citizen` +import integration.steps.given.`Given I have article update notification` import integration.steps.given.`Given I have article` import integration.steps.given.`Given I have citizen` import integration.steps.given.`Given I have follow on article` import integration.steps.given.`authenticated in url as` import io.ktor.http.cio.websocket.Frame import io.ktor.http.cio.websocket.readText -import kotlinx.coroutines.launch import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance -import org.koin.test.get import kotlin.test.assertEquals @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -31,26 +26,7 @@ class `Notification routes` : BaseTest() { `Given I have citizen`("John", "Doe", id = "1a34191a-9cde-45ba-8ac1-230138a102d3") `Given I have article`(id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", createdBy = Name(firstName = "John", lastName = "Doe")) `Given I have follow on article`("John", "Doe", article = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4") - val notification = ArticleUpdateNotification( - ArticleForView( - id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4".toUUID(), - title = "MyTitle", - content = "myContent", - description = "myDescription", - createdBy = CitizenCreator( - id = "1a34191a-9cde-45ba-8ac1-230138a102d3".toUUID(), - name = Name(firstName = "John", lastName = "Doe"), - email = "john-doe@plop.com", - user = UserCreator(username = "john-doe"), - ) - ) - ) - val publisher = get() - launch { - publisher - .publish(notification) - .await() - } + `Given I have article update notification`("a06cbfb7-3094-4d64-aaa1-7486c0c292f4") Thread.sleep(1000) @@ -62,7 +38,41 @@ class `Notification routes` : BaseTest() { ) { incoming, outgoing -> incoming.receive().let { when (it) { - is Frame.Text -> Notification.fromString(it.readText()).let { notif -> + is Frame.Text -> NotificationMessage.fromString(it.readText()).let { notif -> + assertEquals( + "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", + notif.target.id.toString() + ) + outgoing.send(it) + } + else -> error(it.toString()) + } + } + } + } + } + + @Test + fun `I can receive article update notification when follow the creator`() { + withIntegrationApplication { + `Given I have citizen`("Thomas", "Pesquet", id = "1a34191a-9cde-45ba-8ac1-230138a102d3") + `Given I have article`(id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", createdBy = Name(firstName = "Thomas", lastName = "Pesquet")) + `Given I have citizen`("Alan", "Bean") { + `And follow citizen`(Name("Thomas", "Pesquet")) + } + `Given I have article update notification`("a06cbfb7-3094-4d64-aaa1-7486c0c292f4") + + Thread.sleep(1000) + + handleWebSocketConversation( + "/notifications", + { + `authenticated in url as`("Alan", "Bean") + } + ) { incoming, outgoing -> + incoming.receive().let { + when (it) { + is Frame.Text -> NotificationMessage.fromString(it.readText()).let { notif -> assertEquals( "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", notif.target.id.toString() diff --git a/src/test/kotlin/integration/steps/given/Follow.kt b/src/test/kotlin/integration/steps/given/Follow.kt index dfd7a8f..3551e28 100644 --- a/src/test/kotlin/integration/steps/given/Follow.kt +++ b/src/test/kotlin/integration/steps/given/Follow.kt @@ -8,6 +8,7 @@ import fr.dcproject.component.citizen.database.CitizenRef import fr.dcproject.component.citizen.database.CitizenRepository import fr.dcproject.component.constitution.database.ConstitutionRef import fr.dcproject.component.follow.database.FollowArticleRepository +import fr.dcproject.component.follow.database.FollowCitizenRepository import fr.dcproject.component.follow.database.FollowConstitutionRepository import fr.dcproject.component.follow.database.FollowForUpdate import io.ktor.server.testing.TestApplicationEngine @@ -24,6 +25,18 @@ fun Citizen.`And follow constitution`( ) { createFollow(this, ConstitutionRef(constitution.toUUID())) } +fun Citizen.`And follow citizen`( + citizen: String, +) { + createFollow(this, CitizenRef(citizen.toUUID())) +} +fun Citizen.`And follow citizen`( + name: CitizenI.Name, +) { + val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() } + val citizen = citizenRepository.findByName(name) ?: error("Citizen not exist") + createFollow(this, CitizenRef(citizen.id)) +} fun TestApplicationEngine.`Given I have follow on article`( firstName: String, @@ -35,6 +48,17 @@ fun TestApplicationEngine.`Given I have follow on article`( createFollow(citizen, ArticleRef(article.toUUID())) } +fun TestApplicationEngine.`Given I have follow on citizen`( + firstName: String, + lastName: String, + target: CitizenI.Name, +) { + val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() } + val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist") + val targetCitizen = citizenRepository.findByName(target) ?: error("Citizen not exist") + createFollow(citizen, CitizenRef(targetCitizen.id)) +} + fun TestApplicationEngine.`Given I have follow on constitution`( firstName: String, lastName: String, @@ -56,3 +80,9 @@ fun createFollow(citizen: CitizenRef, constitution: ConstitutionRef) { val follow = FollowForUpdate(createdBy = citizen, target = constitution) followConstitutionRepository.follow(follow) } + +fun createFollow(createdBy: CitizenRef, target: CitizenRef) { + val followCitizenRepository: FollowCitizenRepository by lazy { GlobalContext.get().get() } + val follow = FollowForUpdate(createdBy = createdBy, target = target) + followCitizenRepository.follow(follow) +} diff --git a/src/test/kotlin/integration/steps/given/Notification.kt b/src/test/kotlin/integration/steps/given/Notification.kt new file mode 100644 index 0000000..78bd99d --- /dev/null +++ b/src/test/kotlin/integration/steps/given/Notification.kt @@ -0,0 +1,40 @@ +package integration.steps.given + +import fr.dcproject.common.utils.toUUID +import fr.dcproject.component.article.database.ArticleForView +import fr.dcproject.component.auth.database.UserCreator +import fr.dcproject.component.citizen.database.CitizenCreator +import fr.dcproject.component.citizen.database.CitizenI +import fr.dcproject.component.notification.ArticleUpdateNotificationMessage +import fr.dcproject.component.notification.NotificationPublisherAsync +import io.ktor.server.testing.TestApplicationEngine +import kotlinx.coroutines.launch +import org.koin.mp.KoinPlatformTools +import java.util.UUID + +fun TestApplicationEngine.`Given I have article update notification`( + id: String = UUID.randomUUID().toString() +) { + val notification = ArticleUpdateNotificationMessage( + ArticleForView( + id = id.toUUID(), + title = "MyTitle", + content = "myContent", + description = "myDescription", + createdBy = CitizenCreator( + id = "1a34191a-9cde-45ba-8ac1-230138a102d3".toUUID(), + name = CitizenI.Name(firstName = "John", lastName = "Doe"), + email = "john-doe@plop.com", + user = UserCreator(username = "john-doe"), + ) + ) + ) + launch { + KoinPlatformTools + .defaultContext() + .get() + .get() + .publishAsync(notification) + .await() + } +} diff --git a/src/test/resources/sql/follow.sql b/src/test/resources/sql/follow.sql index 0d98afc..c6ed79e 100644 --- a/src/test/resources/sql/follow.sql +++ b/src/test/resources/sql/follow.sql @@ -7,17 +7,19 @@ declare _version_id1 uuid = uuid_generate_v4(); first_article_id uuid := fixture_article(_citizen_id := _citizen_id, _version_id := _version_id1); first_article_updated_id uuid; + _follow_count int = 0; begin perform follow('citizen'::regclass, _citizen_id, _citizen_id2); assert (select count(*) = 1 from follow), 'follow must be inserted'; assert (select following = true from find_follow(_citizen_id, _citizen_id2, 'citizen')), 'find_follow must return the following'; perform follow('citizen'::regclass, _citizen_id, _citizen_id2); - assert (select count(*) = 1 from follow), 'follow must be inserted'; + assert (select count(*) = 1 from follow), 're follow must be do nothing'; perform unfollow('citizen'::regclass, _citizen_id, _citizen_id2); assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow'; + perform follow('article'::regclass, first_article_id, _citizen_id); assert (select following = true from find_follow(first_article_id, _citizen_id, 'article')), 'find_follow must return the following'; assert (select following = false from find_follow(first_article_id, _citizen_id2, 'article')), 'find_follow must not return the following if not followinf'; @@ -29,11 +31,17 @@ begin assert (select following = true from find_follow(first_article_id, _citizen_id, 'article')), '(v1) find_follow must return the following'; assert (select following = true from find_follow(first_article_updated_id, _citizen_id, 'article')), '(v2) find_follow must return the following'; - assert (select f.total = 1 from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return 1 follow'; - assert (select (f.resource#>>'{0, created_by, id}')::uuid = _citizen_id from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return follows with creator'; + assert (select count(*) = 1 from follow), 'must be only 1 follow'; + perform follow('citizen'::regclass, _citizen_id, _citizen_id2); + assert (select count(*) = 2 from follow), 'follow citizen must be inserted'; + assert (select json_array_length(f.resource) = 2 from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return 2 follows'; + assert (select (f.resource#>>'{0, created_by, id}')::uuid = _citizen_id from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return follows with creator'; + assert (select (f.resource#>>'{1, created_by, id}')::uuid = _citizen_id2 from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return follows with creator'; + + _follow_count = (select count(*) from follow); perform unfollow('article'::regclass, first_article_id, _citizen_id); - assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow, event if article is on other version'; + assert (select count(*) = _follow_count-1 from follow), 'follow must be deleted after unfollow, event if article is on other version'; rollback; raise notice 'follow test pass';