#60 Can follow citizen #96
@@ -426,7 +426,7 @@ dependencies {
|
|||||||
testImplementation("io.insert-koin:koin-test:+")
|
testImplementation("io.insert-koin:koin-test:+")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:+")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:+")
|
||||||
testImplementation("io.mockk:mockk:+")
|
testImplementation("io.mockk:mockk:+")
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:+")
|
testImplementation("org.junit.jupiter:junit-jupiter:5.7.+")
|
||||||
testImplementation("org.amshove.kluent:kluent:+")
|
testImplementation("org.amshove.kluent:kluent:+")
|
||||||
testImplementation("io.mockk:mockk-agent-api:+")
|
testImplementation("io.mockk:mockk-agent-api:+")
|
||||||
testImplementation("io.mockk:mockk-agent-jvm:+")
|
testImplementation("io.mockk:mockk-agent-jvm:+")
|
||||||
|
|||||||
388
gradle.lockfile
388
gradle.lockfile
@@ -1,277 +1,131 @@
|
|||||||
# This is a Gradle generated file for dependency locking.
|
# This is a Gradle generated file for dependency locking.
|
||||||
# Manual edits can break the build and are not advised.
|
# Manual edits can break the build and are not advised.
|
||||||
# This file is expected to be part of source control.
|
# 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-classic:1.3.0-alpha5=compileClasspath
|
||||||
ch.qos.logback:logback-core:1.3.0-alpha5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
ch.qos.logback:logback-core:1.3.0-alpha5=compileClasspath
|
||||||
com.auth0:java-jwt:3.15.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.auth0:java-jwt:3.15.0=compileClasspath
|
||||||
com.auth0:jwks-rsa:0.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
com.auth0:jwks-rsa:0.9.0=compileClasspath
|
||||||
com.avast.gradle:gradle-docker-compose-plugin:0.14.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.avast.gradle:gradle-docker-compose-plugin:0.14.3=compileClasspath
|
||||||
com.beust:jcommander:1.81=detekt
|
com.fasterxml.jackson.core:jackson-annotations:2.12.3=compileClasspath
|
||||||
com.fasterxml.jackson.core:jackson-annotations:2.12.2=detekt
|
com.fasterxml.jackson.core:jackson-core:2.12.3=compileClasspath
|
||||||
com.fasterxml.jackson.core:jackson-annotations:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.fasterxml.jackson.core:jackson-databind:2.12.3=compileClasspath
|
||||||
com.fasterxml.jackson.core:jackson-core:2.12.2=detekt
|
com.fasterxml.jackson.datatype:jackson-datatype-joda:2.12.3=compileClasspath
|
||||||
com.fasterxml.jackson.core:jackson-core:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3=compileClasspath
|
||||||
com.fasterxml.jackson.core:jackson-databind:2.12.2=detekt
|
com.fasterxml.jackson:jackson-bom:2.12.3=compileClasspath
|
||||||
com.fasterxml.jackson.core:jackson-databind:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.github.flecomte:postgres-json:2.1.2=compileClasspath
|
||||||
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.github.jasync-sql:jasync-common:1.1.7=compileClasspath
|
||||||
com.fasterxml.jackson.datatype:jackson-datatype-joda:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.github.jasync-sql:jasync-pool:1.1.7=compileClasspath
|
||||||
com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.github.jasync-sql:jasync-postgresql:1.1.7=compileClasspath
|
||||||
com.fasterxml.jackson:jackson-bom:2.12.2=detekt
|
com.googlecode.json-simple:json-simple:1.1.1=compileClasspath
|
||||||
com.fasterxml.jackson:jackson-bom:2.12.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.jayway.jsonpath:json-path:2.5.0=compileClasspath
|
||||||
com.github.flecomte:postgres-json:2.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.ongres.scram:client:2.1=compileClasspath
|
||||||
com.github.jasync-sql:jasync-common:1.1.7=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.ongres.scram:common:2.1=compileClasspath
|
||||||
com.github.jasync-sql:jasync-pool:1.1.7=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.ongres.stringprep:saslprep:1.1=compileClasspath
|
||||||
com.github.jasync-sql:jasync-postgresql:1.1.7=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
com.ongres.stringprep:stringprep:1.1=compileClasspath
|
||||||
com.github.shyiko.klob:klob:0.2.1=ktlint
|
com.rabbitmq:amqp-client:5.12.0=compileClasspath
|
||||||
com.google.code.findbugs:jsr305:3.0.2=runtimeClasspath,testRuntimeClasspath
|
com.sendgrid:java-http-client:4.3.6=compileClasspath
|
||||||
com.google.errorprone:error_prone_annotations:2.2.0=runtimeClasspath,testRuntimeClasspath
|
com.sendgrid:sendgrid-java:4.7.1=compileClasspath
|
||||||
com.google.guava:failureaccess:1.0.1=runtimeClasspath,testRuntimeClasspath
|
com.typesafe:config:1.3.1=compileClasspath
|
||||||
com.google.guava:guava:27.1-jre=runtimeClasspath,testRuntimeClasspath
|
commons-codec:commons-codec:1.11=compileClasspath
|
||||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=runtimeClasspath,testRuntimeClasspath
|
commons-logging:commons-logging:1.2=compileClasspath
|
||||||
com.google.j2objc:j2objc-annotations:1.1=runtimeClasspath,testRuntimeClasspath
|
edu.washington.cs.types.checker:checker-framework:1.7.0=compileClasspath
|
||||||
com.googlecode.json-simple:json-simple:1.1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
io.github.microutils:kotlin-logging:1.7.6=compileClasspath
|
||||||
com.jayway.jsonpath:json-path:2.5.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.insert-koin:koin-core-ext:3.0.1=compileClasspath
|
||||||
com.ongres.scram:client:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.insert-koin:koin-core-jvm:3.0.1=compileClasspath
|
||||||
com.ongres.scram:common:2.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.insert-koin:koin-core:3.0.1=compileClasspath
|
||||||
com.ongres.stringprep:saslprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.insert-koin:koin-ktor:3.0.1=compileClasspath
|
||||||
com.ongres.stringprep:stringprep:1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.konform:konform-jvm:0.3.0-RC1=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-core:0.40.0=ktlint
|
io.ktor:ktor-auth-jwt:1.5.3=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-reporter-baseline:0.40.0=ktlint
|
io.ktor:ktor-auth-kotlinMultiplatform:1.5.3=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-reporter-checkstyle:0.40.0=ktlint
|
io.ktor:ktor-auth:1.5.3=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-reporter-html:0.40.0=ktlint
|
io.ktor:ktor-client-core-jvm:1.5.3=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-reporter-json:0.40.0=ktlint
|
io.ktor:ktor-client-core:1.5.3=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-reporter-plain:0.40.0=ktlint
|
io.ktor:ktor-client-jetty:1.5.3=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-ruleset-experimental:0.40.0=ktlint
|
io.ktor:ktor-http-cio-jvm:1.5.3=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-ruleset-standard:0.40.0=ktlint
|
io.ktor:ktor-http-cio:1.5.3=compileClasspath
|
||||||
com.pinterest.ktlint:ktlint-ruleset-test:0.40.0=ktlint
|
io.ktor:ktor-http-jvm:1.5.3=compileClasspath
|
||||||
com.pinterest:ktlint:0.40.0=ktlint
|
io.ktor:ktor-http:1.5.3=compileClasspath
|
||||||
com.rabbitmq:amqp-client:5.12.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.ktor:ktor-io-jvm:1.5.3=compileClasspath
|
||||||
com.sendgrid:java-http-client:4.3.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.ktor:ktor-io:1.5.3=compileClasspath
|
||||||
com.sendgrid:sendgrid-java:4.7.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.ktor:ktor-jackson:1.5.3=compileClasspath
|
||||||
com.sun.mail:javax.mail:1.6.2=runtimeClasspath,testRuntimeClasspath
|
io.ktor:ktor-locations:1.5.3=compileClasspath
|
||||||
com.thedeanda:lorem:2.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.ktor:ktor-network-jvm:1.5.3=compileClasspath
|
||||||
com.typesafe:config:1.3.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
io.ktor:ktor-network:1.5.3=compileClasspath
|
||||||
commons-codec:commons-codec:1.11=compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata
|
io.ktor:ktor-server-core-kotlinMultiplatform:1.5.3=compileClasspath
|
||||||
commons-codec:commons-codec:1.13=runtimeClasspath,testRuntimeClasspath
|
io.ktor:ktor-server-core:1.5.3=compileClasspath
|
||||||
commons-io:commons-io:2.6=runtimeClasspath,testRuntimeClasspath
|
io.ktor:ktor-server-host-common-kotlinMultiplatform:1.5.3=compileClasspath
|
||||||
commons-logging:commons-logging:1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.ktor:ktor-server-host-common:1.5.3=compileClasspath
|
||||||
edu.washington.cs.types.checker:checker-framework:1.7.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.ktor:ktor-server-jetty:1.5.3=compileClasspath
|
||||||
info.picocli:picocli:3.9.6=ktlint
|
io.ktor:ktor-server-servlet-kotlinMultiplatform:1.5.3=compileClasspath
|
||||||
io.github.detekt.sarif4j:sarif4j:1.0.0=detekt
|
io.ktor:ktor-server-servlet:1.5.3=compileClasspath
|
||||||
io.github.microutils:kotlin-logging:1.7.6=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
io.ktor:ktor-utils-jvm:1.5.3=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-api:1.16.0=detekt
|
io.ktor:ktor-utils:1.5.3=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-bom:1.16.0=detekt
|
io.ktor:ktor-websockets:1.5.3=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-cli:1.16.0=detekt
|
io.lettuce:lettuce-core:5.3.6.RELEASE=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-core:1.16.0=detekt
|
io.netty:netty-buffer:4.1.56.Final=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-metrics:1.16.0=detekt
|
io.netty:netty-codec:4.1.56.Final=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-parser:1.16.0=detekt
|
io.netty:netty-common:4.1.56.Final=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-psi-utils:1.16.0=detekt
|
io.netty:netty-handler:4.1.56.Final=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-report-html:1.16.0=detekt
|
io.netty:netty-resolver:4.1.56.Final=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-report-sarif:1.16.0=detekt
|
io.netty:netty-transport:4.1.56.Final=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-report-txt:1.16.0=detekt
|
io.projectreactor:reactor-core:3.4.1=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-report-xml:1.16.0=detekt
|
javax.servlet:javax.servlet-api:3.1.0=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-complexity:1.16.0=detekt
|
joda-time:joda-time:2.10.8=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-coroutines:1.16.0=detekt
|
net.minidev:accessors-smart:1.2=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-documentation:1.16.0=detekt
|
net.minidev:json-smart:2.3=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-empty:1.16.0=detekt
|
net.pearx.kasechange:kasechange-jvm:1.3.0=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-errorprone:1.16.0=detekt
|
org.apache.httpcomponents:httpasyncclient:4.1.2=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-exceptions:1.16.0=detekt
|
org.apache.httpcomponents:httpclient:4.5.12=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-naming:1.16.0=detekt
|
org.apache.httpcomponents:httpcore-nio:4.4.5=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-performance:1.16.0=detekt
|
org.apache.httpcomponents:httpcore:4.4.13=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules-style:1.16.0=detekt
|
org.bouncycastle:bcprov-jdk15on:1.67=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-rules:1.16.0=detekt
|
org.eclipse.jetty.http2:http2-client:9.4.31.v20200723=compileClasspath
|
||||||
io.gitlab.arturbosch.detekt:detekt-tooling:1.16.0=detekt
|
org.eclipse.jetty.http2:http2-common:9.4.31.v20200723=compileClasspath
|
||||||
io.insert-koin:koin-core-ext:3.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.eclipse.jetty.http2:http2-hpack:9.4.31.v20200723=compileClasspath
|
||||||
io.insert-koin:koin-core-jvm:3.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
org.eclipse.jetty.http2:http2-server:9.4.31.v20200723=compileClasspath
|
||||||
io.insert-koin:koin-core:3.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.eclipse.jetty:jetty-alpn-client:9.4.31.v20200723=compileClasspath
|
||||||
io.insert-koin:koin-ktor:3.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.eclipse.jetty:jetty-alpn-java-client:9.4.31.v20200723=compileClasspath
|
||||||
io.insert-koin:koin-test-jvm:3.0.1=testCompileClasspath,testRuntimeClasspath
|
org.eclipse.jetty:jetty-alpn-java-server:9.4.31.v20200723=compileClasspath
|
||||||
io.insert-koin:koin-test:3.0.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.eclipse.jetty:jetty-alpn-openjdk8-client:9.4.31.v20200723=compileClasspath
|
||||||
io.konform:konform-jvm:0.3.0-RC1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.eclipse.jetty:jetty-alpn-openjdk8-server:9.4.31.v20200723=compileClasspath
|
||||||
io.konform:konform-metadata:0.3.0-RC1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
|
org.eclipse.jetty:jetty-alpn-server:9.4.31.v20200723=compileClasspath
|
||||||
io.ktor:ktor-auth-jwt-kotlinMultiplatform:1.5.3=implementationDependenciesMetadata,testImplementationDependenciesMetadata
|
org.eclipse.jetty:jetty-continuation:9.4.31.v20200723=compileClasspath
|
||||||
io.ktor:ktor-auth-jwt:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.eclipse.jetty:jetty-http:9.4.31.v20200723=compileClasspath
|
||||||
io.ktor:ktor-auth-kotlinMultiplatform:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.eclipse.jetty:jetty-io:9.4.31.v20200723=compileClasspath
|
||||||
io.ktor:ktor-auth:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.eclipse.jetty:jetty-server:9.4.31.v20200723=compileClasspath
|
||||||
io.ktor:ktor-client-cio-jvm:1.5.3=testCompileClasspath,testRuntimeClasspath
|
org.eclipse.jetty:jetty-servlets:9.4.31.v20200723=compileClasspath
|
||||||
io.ktor:ktor-client-cio:1.5.3=testCompileClasspath,testRuntimeClasspath
|
org.eclipse.jetty:jetty-util:9.4.31.v20200723=compileClasspath
|
||||||
io.ktor:ktor-client-core-jvm:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
org.elasticsearch.client:elasticsearch-rest-client:6.8.15=compileClasspath
|
||||||
io.ktor:ktor-client-core:1.5.3=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.intellij.deps:trove4j:1.0.20181211=kotlinCompilerClasspath
|
||||||
io.ktor:ktor-client-jetty-kotlinMultiplatform:1.5.3=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.32=kotlinCompilerClasspath
|
||||||
io.ktor:ktor-client-jetty:1.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.4.32=kotlinCompilerClasspath
|
||||||
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
|
|
||||||
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.4.32=kotlinCompilerPluginClasspath
|
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.32=compileClasspath,kotlinCompilerClasspath
|
||||||
org.jetbrains.kotlin:kotlin-reflect:1.4.10=ktlint
|
org.jetbrains.kotlin:kotlin-script-runtime:1.4.32=kotlinCompilerClasspath,kotlinCompilerPluginClasspath
|
||||||
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-scripting-common:1.4.32=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-embeddable:1.4.32=kotlinCompilerPluginClasspath
|
||||||
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-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-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-serialization:1.4.32=kotlinCompilerPluginClasspath
|
||||||
org.jetbrains.kotlin:kotlin-stdlib-common:1.4.10=ktlint
|
org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32=kotlinCompilerClasspath,kotlinCompilerPluginClasspath
|
||||||
org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21=detekt
|
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.0=compileClasspath
|
||||||
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.5.0=compileClasspath
|
||||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21=detekt
|
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.0=compileClasspath
|
||||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.32=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.kotlin:kotlin-stdlib:1.4.32=kotlinCompilerClasspath,kotlinCompilerPluginClasspath
|
||||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21=detekt
|
org.jetbrains.kotlin:kotlin-stdlib:1.5.0=compileClasspath
|
||||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0-RC=compileClasspath
|
||||||
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.kotlinx:kotlinx-coroutines-core:1.3.8=kotlinCompilerPluginClasspath
|
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.5.0-RC=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt=compileClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3-native-mt=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.4.3-native-mt=testRuntimeClasspath
|
org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.0-RC=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3-native-mt=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.5.0-RC=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.4.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.2.0=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.4.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.kotlinx:kotlinx-serialization-core:1.2.0=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.2.0=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2=detekt
|
org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
org.jetbrains:annotations:13.0=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-serialization-core-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
|
org.joda:joda-convert:1.8.1=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
org.ow2.asm:asm:5.0.4=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
org.reactivestreams:reactive-streams:1.0.3=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-serialization-json-metadata:1.0.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata
|
org.slf4j:slf4j-api:2.0.0-alpha1=compileClasspath
|
||||||
org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
empty=
|
||||||
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
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.datatype.joda.JodaModule
|
|||||||
import fr.dcproject.application.Env.PROD
|
import fr.dcproject.application.Env.PROD
|
||||||
import fr.dcproject.application.Env.TEST
|
import fr.dcproject.application.Env.TEST
|
||||||
import fr.dcproject.application.http.statusPagesInstallation
|
import fr.dcproject.application.http.statusPagesInstallation
|
||||||
|
import fr.dcproject.common.utils.onApplicationStopped
|
||||||
import fr.dcproject.component.article.articleKoinModule
|
import fr.dcproject.component.article.articleKoinModule
|
||||||
import fr.dcproject.component.article.routes.installArticleRoutes
|
import fr.dcproject.component.article.routes.installArticleRoutes
|
||||||
import fr.dcproject.component.auth.authKoinModule
|
import fr.dcproject.component.auth.authKoinModule
|
||||||
@@ -25,8 +26,10 @@ import fr.dcproject.component.constitution.routes.installConstitutionRoutes
|
|||||||
import fr.dcproject.component.doc.routes.installDocRoutes
|
import fr.dcproject.component.doc.routes.installDocRoutes
|
||||||
import fr.dcproject.component.follow.followKoinModule
|
import fr.dcproject.component.follow.followKoinModule
|
||||||
import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes
|
import fr.dcproject.component.follow.routes.article.installFollowArticleRoutes
|
||||||
|
import fr.dcproject.component.follow.routes.citizen.installFollowCitizenRoutes
|
||||||
import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes
|
import fr.dcproject.component.follow.routes.constitution.installFollowConstitutionRoutes
|
||||||
import fr.dcproject.component.notification.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.notification.routes.installNotificationsRoutes
|
||||||
import fr.dcproject.component.opinion.opinionKoinModule
|
import fr.dcproject.component.opinion.opinionKoinModule
|
||||||
import fr.dcproject.component.opinion.routes.installOpinionRoutes
|
import fr.dcproject.component.opinion.routes.installOpinionRoutes
|
||||||
@@ -37,7 +40,6 @@ import fr.dcproject.component.workgroup.routes.installWorkgroupRoutes
|
|||||||
import fr.dcproject.component.workgroup.workgroupKoinModule
|
import fr.dcproject.component.workgroup.workgroupKoinModule
|
||||||
import fr.postgresjson.migration.Migrations
|
import fr.postgresjson.migration.Migrations
|
||||||
import io.ktor.application.Application
|
import io.ktor.application.Application
|
||||||
import io.ktor.application.ApplicationStopped
|
|
||||||
import io.ktor.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.auth.Authentication
|
import io.ktor.auth.Authentication
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
@@ -117,11 +119,14 @@ fun Application.module(env: Env = PROD) {
|
|||||||
masking = false
|
masking = false
|
||||||
}
|
}
|
||||||
|
|
||||||
get<NotificationConsumer>().run {
|
get<NotificationEmailConsumer>().run {
|
||||||
start()
|
start()
|
||||||
environment.monitor.subscribe(ApplicationStopped) {
|
onApplicationStopped { close() }
|
||||||
close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get<NotificationPushConsumer>().run {
|
||||||
|
start()
|
||||||
|
onApplicationStopped { close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
install(Authentication, jwtInstallation(get(), get()))
|
install(Authentication, jwtInstallation(get(), get()))
|
||||||
@@ -154,6 +159,7 @@ fun Application.module(env: Env = PROD) {
|
|||||||
installCommentRoutes()
|
installCommentRoutes()
|
||||||
installFollowArticleRoutes()
|
installFollowArticleRoutes()
|
||||||
installFollowConstitutionRoutes()
|
installFollowConstitutionRoutes()
|
||||||
|
installFollowCitizenRoutes()
|
||||||
installWorkgroupRoutes()
|
installWorkgroupRoutes()
|
||||||
installOpinionRoutes()
|
installOpinionRoutes()
|
||||||
installVoteRoutes()
|
installVoteRoutes()
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|||||||
import com.rabbitmq.client.ConnectionFactory
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import fr.dcproject.common.email.Mailer
|
import fr.dcproject.common.email.Mailer
|
||||||
import fr.dcproject.component.auth.jwt.JwtConfig
|
import fr.dcproject.component.auth.jwt.JwtConfig
|
||||||
import fr.dcproject.component.notification.NotificationConsumer
|
import fr.dcproject.component.notification.NotificationPublisherAsync
|
||||||
import fr.dcproject.component.notification.NotificationEmailSender
|
import fr.dcproject.component.notification.email.NotificationEmailConsumer
|
||||||
import fr.dcproject.component.notification.NotificationsPush
|
import fr.dcproject.component.notification.email.NotificationEmailSender
|
||||||
import fr.dcproject.component.notification.Publisher
|
import fr.dcproject.component.notification.push.NotificationPushConsumer
|
||||||
|
import fr.dcproject.component.notification.push.NotificationPushListener
|
||||||
import fr.postgresjson.connexion.Connection
|
import fr.postgresjson.connexion.Connection
|
||||||
import fr.postgresjson.connexion.Requester
|
import fr.postgresjson.connexion.Requester
|
||||||
import fr.postgresjson.migration.Migrations
|
import fr.postgresjson.migration.Migrations
|
||||||
@@ -65,11 +66,15 @@ val KoinModule = module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
single { NotificationsPush.Builder(get()) }
|
single { NotificationPushListener.Builder(get()) }
|
||||||
|
|
||||||
single {
|
single {
|
||||||
val config: Configuration = get()
|
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
|
// RabbitMQ
|
||||||
@@ -114,7 +119,7 @@ val KoinModule = module {
|
|||||||
|
|
||||||
single {
|
single {
|
||||||
val config: Configuration = get()
|
val config: Configuration = get()
|
||||||
Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
|
NotificationPublisherAsync(factory = get(), exchangeName = config.exchangeNotificationName)
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import java.io.IOException
|
|||||||
class Mailer(
|
class Mailer(
|
||||||
private val key: String
|
private val key: String
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* Send email via Sendgrid
|
||||||
|
*/
|
||||||
fun sendEmail(action: () -> Mail): Boolean {
|
fun sendEmail(action: () -> Mail): Boolean {
|
||||||
val mail = action()
|
val mail = action()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package fr.dcproject.common.entity
|
|||||||
|
|
||||||
import fr.dcproject.component.article.database.ArticleRef
|
import fr.dcproject.component.article.database.ArticleRef
|
||||||
import fr.dcproject.component.citizen.database.CitizenI
|
import fr.dcproject.component.citizen.database.CitizenI
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenRef
|
||||||
import fr.dcproject.component.comment.generic.database.CommentRef
|
import fr.dcproject.component.comment.generic.database.CommentRef
|
||||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||||
import fr.dcproject.component.opinion.database.OpinionRef
|
import fr.dcproject.component.opinion.database.OpinionRef
|
||||||
@@ -34,7 +35,8 @@ interface TargetI : EntityI {
|
|||||||
Article("article"),
|
Article("article"),
|
||||||
Constitution("constitution"),
|
Constitution("constitution"),
|
||||||
Comment("comment"),
|
Comment("comment"),
|
||||||
Opinion("opinion")
|
Opinion("opinion"),
|
||||||
|
Citizen("citizen"),
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -44,6 +46,7 @@ interface TargetI : EntityI {
|
|||||||
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
|
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
|
||||||
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
|
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
|
||||||
t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference
|
t.isSubclassOf(OpinionRef::class) -> TargetName.Opinion.targetReference
|
||||||
|
t.isSubclassOf(CitizenRef::class) -> TargetName.Citizen.targetReference
|
||||||
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
|
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/main/kotlin/fr/dcproject/common/utils/Ktor.kt
Normal file
10
src/main/kotlin/fr/dcproject/common/utils/Ktor.kt
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/kotlin/fr/dcproject/common/utils/RabbitConsume.kt
Normal file
30
src/main/kotlin/fr/dcproject/common/utils/RabbitConsume.kt
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.
|
|||||||
import fr.dcproject.component.auth.citizen
|
import fr.dcproject.component.auth.citizen
|
||||||
import fr.dcproject.component.auth.citizenOrNull
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
import fr.dcproject.component.auth.mustBeAuth
|
import fr.dcproject.component.auth.mustBeAuth
|
||||||
import fr.dcproject.component.notification.ArticleUpdateNotification
|
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage
|
||||||
import fr.dcproject.component.notification.Publisher
|
import fr.dcproject.component.notification.NotificationPublisherAsync
|
||||||
import fr.dcproject.component.workgroup.database.WorkgroupRef
|
import fr.dcproject.component.workgroup.database.WorkgroupRef
|
||||||
import io.konform.validation.Validation
|
import io.konform.validation.Validation
|
||||||
import io.konform.validation.jsonschema.maxItems
|
import io.konform.validation.jsonschema.maxItems
|
||||||
@@ -63,7 +63,7 @@ object UpsertArticle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.upsertArticle(repo: ArticleRepository, publisher: Publisher, ac: ArticleAccessControl) {
|
fun Route.upsertArticle(repo: ArticleRepository, notificationPublisher: NotificationPublisherAsync, ac: ArticleAccessControl) {
|
||||||
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest<Input>().run {
|
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receiveOrBadRequest<Input>().run {
|
||||||
validate().badRequestIfNotValid()
|
validate().badRequestIfNotValid()
|
||||||
ArticleForUpdate(
|
ArticleForUpdate(
|
||||||
@@ -92,7 +92,7 @@ object UpsertArticle {
|
|||||||
val versionNumber = a.versionNumber
|
val versionNumber = a.versionNumber
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
publisher.publish(ArticleUpdateNotification(a))
|
notificationPublisher.publishAsync(ArticleUpdateNotificationMessage(a))
|
||||||
} ?: error("Article not updated")
|
} ?: error("Article not updated")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package fr.dcproject.component.citizen.database
|
|||||||
|
|
||||||
import fr.dcproject.common.entity.CreatedAt
|
import fr.dcproject.common.entity.CreatedAt
|
||||||
import fr.dcproject.common.entity.DeletedAt
|
import fr.dcproject.common.entity.DeletedAt
|
||||||
import fr.dcproject.common.entity.Entity
|
|
||||||
import fr.dcproject.common.entity.EntityI
|
import fr.dcproject.common.entity.EntityI
|
||||||
|
import fr.dcproject.common.entity.TargetI
|
||||||
|
import fr.dcproject.common.entity.TargetRef
|
||||||
import fr.dcproject.component.auth.database.User
|
import fr.dcproject.component.auth.database.User
|
||||||
import fr.dcproject.component.auth.database.UserCreator
|
import fr.dcproject.component.auth.database.UserCreator
|
||||||
import fr.dcproject.component.auth.database.UserForCreate
|
import fr.dcproject.component.auth.database.UserForCreate
|
||||||
@@ -95,10 +96,10 @@ open class CitizenRefWithUser(
|
|||||||
|
|
||||||
open class CitizenRef(
|
open class CitizenRef(
|
||||||
id: UUID = UUID.randomUUID()
|
id: UUID = UUID.randomUUID()
|
||||||
) : Entity(id),
|
) : TargetRef(id),
|
||||||
CitizenI
|
CitizenI
|
||||||
|
|
||||||
interface CitizenI : EntityI {
|
interface CitizenI : EntityI, TargetI {
|
||||||
data class Name(
|
data class Name(
|
||||||
override val firstName: String,
|
override val firstName: String,
|
||||||
override val lastName: String,
|
override val lastName: String,
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package fr.dcproject.component.follow
|
package fr.dcproject.component.follow
|
||||||
|
|
||||||
import fr.dcproject.component.follow.database.FollowArticleRepository
|
import fr.dcproject.component.follow.database.FollowArticleRepository
|
||||||
|
import fr.dcproject.component.follow.database.FollowCitizenRepository
|
||||||
import fr.dcproject.component.follow.database.FollowConstitutionRepository
|
import fr.dcproject.component.follow.database.FollowConstitutionRepository
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val followKoinModule = module {
|
val followKoinModule = module {
|
||||||
single { FollowArticleRepository(get()) }
|
single { FollowArticleRepository(get()) }
|
||||||
single { FollowConstitutionRepository(get()) }
|
single { FollowConstitutionRepository(get()) }
|
||||||
|
single { FollowCitizenRepository(get()) }
|
||||||
single { FollowAccessControl() }
|
single { FollowAccessControl() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import fr.dcproject.common.entity.Entity
|
|||||||
import fr.dcproject.common.entity.TargetRef
|
import fr.dcproject.common.entity.TargetRef
|
||||||
import fr.dcproject.component.article.database.ArticleForView
|
import fr.dcproject.component.article.database.ArticleForView
|
||||||
import fr.dcproject.component.article.database.ArticleRef
|
import fr.dcproject.component.article.database.ArticleRef
|
||||||
|
import fr.dcproject.component.citizen.database.Citizen
|
||||||
import fr.dcproject.component.citizen.database.CitizenI
|
import fr.dcproject.component.citizen.database.CitizenI
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenRef
|
||||||
import fr.dcproject.component.constitution.database.ConstitutionForView
|
import fr.dcproject.component.constitution.database.ConstitutionForView
|
||||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||||
import fr.postgresjson.connexion.Paginated
|
import fr.postgresjson.connexion.Paginated
|
||||||
@@ -72,21 +74,24 @@ sealed class FollowRepository<IN : TargetRef, OUT : TargetRef>(override var requ
|
|||||||
target: Entity,
|
target: Entity,
|
||||||
bulkSize: Int = 300
|
bulkSize: Int = 300
|
||||||
): Flow<FollowForView<IN>> = flow {
|
): Flow<FollowForView<IN>> = flow {
|
||||||
var nextPage = 1
|
var lastId: UUID? = null
|
||||||
do {
|
while (true) {
|
||||||
val paginate = findFollowsByTarget(target, nextPage, bulkSize)
|
val result = findFollowsByTarget(target, lastId, bulkSize)
|
||||||
paginate.result.forEach {
|
if (result.count() == 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result.forEach {
|
||||||
emit(it)
|
emit(it)
|
||||||
}
|
}
|
||||||
nextPage = paginate.currentPage + 1
|
lastId = result.last().id
|
||||||
} while (!paginate.isLastPage())
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun findFollowsByTarget(
|
abstract fun findFollowsByTarget(
|
||||||
target: Entity,
|
target: Entity,
|
||||||
page: Int = 1,
|
lastId: UUID?,
|
||||||
limit: Int = 300
|
limit: Int = 300
|
||||||
): Paginated<FollowForView<IN>>
|
): List<FollowForView<IN>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
|
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
|
||||||
@@ -107,14 +112,14 @@ class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRe
|
|||||||
|
|
||||||
override fun findFollowsByTarget(
|
override fun findFollowsByTarget(
|
||||||
target: Entity,
|
target: Entity,
|
||||||
page: Int,
|
lastId: UUID?,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Paginated<FollowForView<ArticleRef>> {
|
): List<FollowForView<ArticleRef>> {
|
||||||
return requester
|
return requester
|
||||||
.getFunction("find_follows_article_by_target")
|
.getFunction("find_follows_article_by_target")
|
||||||
.select(
|
.select(
|
||||||
page,
|
"start_id" to lastId,
|
||||||
limit,
|
"limit" to limit,
|
||||||
"target_id" to target.id
|
"target_id" to target.id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,9 +143,34 @@ class FollowConstitutionRepository(requester: Requester) : FollowRepository<Cons
|
|||||||
|
|
||||||
override fun findFollowsByTarget(
|
override fun findFollowsByTarget(
|
||||||
target: Entity,
|
target: Entity,
|
||||||
page: Int,
|
lastId: UUID?,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Paginated<FollowForView<ConstitutionRef>> {
|
): List<FollowForView<ConstitutionRef>> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FollowCitizenRepository(requester: Requester) : FollowRepository<CitizenRef, Citizen>(requester) {
|
||||||
|
override fun findByCitizen(
|
||||||
|
citizenId: UUID,
|
||||||
|
page: Int,
|
||||||
|
limit: Int
|
||||||
|
): Paginated<FollowForView<Citizen>> {
|
||||||
|
return requester.run {
|
||||||
|
getFunction("find_follows_citizen_by_citizen")
|
||||||
|
.select(
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
"created_by_id" to citizenId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findFollowsByTarget(
|
||||||
|
target: Entity,
|
||||||
|
lastId: UUID?,
|
||||||
|
limit: Int
|
||||||
|
): List<FollowForView<CitizenRef>> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import fr.dcproject.component.auth.citizen
|
|||||||
import fr.dcproject.component.auth.citizenOrNull
|
import fr.dcproject.component.auth.citizenOrNull
|
||||||
import fr.dcproject.component.follow.FollowAccessControl
|
import fr.dcproject.component.follow.FollowAccessControl
|
||||||
import fr.dcproject.component.follow.database.FollowArticleRepository
|
import fr.dcproject.component.follow.database.FollowArticleRepository
|
||||||
|
import fr.dcproject.component.follow.routes.citizen.toOutput
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
@@ -14,7 +15,6 @@ import io.ktor.locations.Location
|
|||||||
import io.ktor.locations.get
|
import io.ktor.locations.get
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import org.joda.time.DateTime
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
@@ -30,19 +30,7 @@ object GetFollowArticle {
|
|||||||
ac.assert { canView(follow, citizenOrNull) }
|
ac.assert { canView(follow, citizenOrNull) }
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
follow.let { f ->
|
follow.toOutput()
|
||||||
object {
|
|
||||||
val id: UUID = f.id
|
|
||||||
val createdBy: Any = f.createdBy.toOutput()
|
|
||||||
val target: Any = f.target.let { t ->
|
|
||||||
object {
|
|
||||||
val id: UUID = t.id
|
|
||||||
val reference: String = f.target.reference
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val createdAt: DateTime = f.createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
} ?: call.respond(HttpStatusCode.NoContent)
|
} ?: call.respond(HttpStatusCode.NoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<CitizenFollowRequest> {
|
||||||
|
mustBeAuth()
|
||||||
|
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
|
||||||
|
ac.assert { canCreate(follow, citizenOrNull) }
|
||||||
|
repo.follow(follow)
|
||||||
|
call.respond(HttpStatusCode.Created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<CitizenFollowRequest> {
|
||||||
|
repo.findFollow(citizen, it.citizen)?.let { follow ->
|
||||||
|
ac.assert { canView(follow, citizenOrNull) }
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.OK,
|
||||||
|
follow.toOutput()
|
||||||
|
)
|
||||||
|
} ?: call.respond(HttpStatusCode.NoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<CitizenFollowCitizenRequest> {
|
||||||
|
mustBeAuth()
|
||||||
|
val follows = repo.findByCitizen(it.citizen)
|
||||||
|
ac.assert { canView(follows.result, citizenOrNull) }
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.OK,
|
||||||
|
follows.toOutput { f ->
|
||||||
|
object {
|
||||||
|
val id: UUID = f.id
|
||||||
|
val createdBy: Any = f.createdBy.toOutput()
|
||||||
|
val target: Any = f.target.let { t ->
|
||||||
|
object {
|
||||||
|
val id: UUID = t.id
|
||||||
|
val reference: String = f.target.reference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val createdAt: DateTime = f.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<CitizenFollowRequest> {
|
||||||
|
mustBeAuth()
|
||||||
|
val follow = FollowForUpdate(target = it.citizen, createdBy = this.citizen)
|
||||||
|
ac.assert { canDelete(follow, citizenOrNull) }
|
||||||
|
repo.unfollow(follow)
|
||||||
|
call.respond(HttpStatusCode.NoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import fr.dcproject.component.auth.citizenOrNull
|
|||||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||||
import fr.dcproject.component.follow.FollowAccessControl
|
import fr.dcproject.component.follow.FollowAccessControl
|
||||||
import fr.dcproject.component.follow.database.FollowConstitutionRepository
|
import fr.dcproject.component.follow.database.FollowConstitutionRepository
|
||||||
|
import fr.dcproject.component.follow.routes.citizen.toOutput
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
@@ -14,7 +15,6 @@ import io.ktor.locations.Location
|
|||||||
import io.ktor.locations.get
|
import io.ktor.locations.get
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import org.joda.time.DateTime
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
@@ -30,19 +30,7 @@ object GetFollowConstitution {
|
|||||||
ac.assert { canView(follow, citizenOrNull) }
|
ac.assert { canView(follow, citizenOrNull) }
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
follow.let { f ->
|
follow.toOutput()
|
||||||
object {
|
|
||||||
val id: UUID = f.id
|
|
||||||
val createdBy: Any = f.createdBy.toOutput()
|
|
||||||
val target: Any = f.target.let { t ->
|
|
||||||
object {
|
|
||||||
val id: UUID = t.id
|
|
||||||
val reference: String = f.target.reference
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val createdAt: DateTime = f.createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
} ?: call.respond(HttpStatusCode.NotFound)
|
} ?: call.respond(HttpStatusCode.NotFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
|
|
||||||
private val rabbitConnection = rabbitFactory.newConnection()
|
|
||||||
private val rabbitChannel = rabbitConnection.createChannel()
|
|
||||||
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
rabbitChannel.close()
|
|
||||||
rabbitConnection.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
/* Config Rabbit */
|
|
||||||
rabbitFactory.newConnection().use { connection ->
|
|
||||||
connection.createChannel().use { channel ->
|
|
||||||
channel.queueDeclare("push", true, false, false, null)
|
|
||||||
channel.queueDeclare("email", true, false, false, null)
|
|
||||||
channel.exchangeDeclare(exchangeName, DIRECT, true)
|
|
||||||
channel.queueBind("push", exchangeName, "")
|
|
||||||
channel.queueBind("email", exchangeName, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Define Consumer */
|
|
||||||
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun handleDelivery(
|
|
||||||
consumerTag: String,
|
|
||||||
envelope: Envelope,
|
|
||||||
properties: BasicProperties,
|
|
||||||
body: ByteArray
|
|
||||||
) = runBlocking {
|
|
||||||
followersFromMessage(body) {
|
|
||||||
redis.zadd(
|
|
||||||
"notification:${it.follow.createdBy.id}",
|
|
||||||
it.event.id,
|
|
||||||
it.rawMessage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
rabbitChannel.basicAck(envelope.deliveryTag, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val consumerEmail: Consumer = object : DefaultConsumer(rabbitChannel) {
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun handleDelivery(
|
|
||||||
consumerTag: String,
|
|
||||||
envelope: Envelope,
|
|
||||||
properties: BasicProperties,
|
|
||||||
body: ByteArray
|
|
||||||
) {
|
|
||||||
runBlocking {
|
|
||||||
followersFromMessage(body) {
|
|
||||||
notificationEmailSender.sendEmail(it.follow)
|
|
||||||
logger.debug("EmailSend to: ${it.follow.createdBy.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rabbitChannel.basicAck(envelope.deliveryTag, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Launch Consumer */
|
|
||||||
rabbitChannel.basicConsume("push", false, consumerPush) // The front consume the redis via Websocket
|
|
||||||
rabbitChannel.basicConsume("email", false, consumerEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun followersFromMessage(body: ByteArray, action: suspend (DecodedMessage) -> Unit) {
|
|
||||||
val rawMessage: String = body.toString(Charsets.UTF_8)
|
|
||||||
val notification: EntityNotification = Notification.fromString(rawMessage)
|
|
||||||
val follows = when (notification.type) {
|
|
||||||
"article" -> followArticleRepo.findFollowsByTarget(notification.target)
|
|
||||||
"constitution" -> followConstitutionRepo.findFollowsByTarget(notification.target)
|
|
||||||
else -> error("event '${notification.type}' not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
follows.collect { action(DecodedMessage(notification, rawMessage, it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DecodedMessage(
|
|
||||||
val event: EntityNotification,
|
|
||||||
val rawMessage: String,
|
|
||||||
val follow: FollowForView<out TargetRef>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 <E : Entity> (
|
||||||
|
val event: EntityNotificationMessage<E>,
|
||||||
|
val rawMessage: String,
|
||||||
|
val follow: FollowForView<out TargetRef>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<out TargetRef>) {
|
|
||||||
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
|
|
||||||
val target = when (follow.target.reference) {
|
|
||||||
"article" ->
|
|
||||||
articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
|
|
||||||
else -> noTarget(follow.target.id)
|
|
||||||
}
|
|
||||||
val subject = when (follow.target.reference) {
|
|
||||||
"article" -> """New version for article "${target.title}""""
|
|
||||||
else -> "Notification"
|
|
||||||
}
|
|
||||||
mailer.sendEmail {
|
|
||||||
Mail(
|
|
||||||
Email("notification@$domain"),
|
|
||||||
subject,
|
|
||||||
Email(citizen.email),
|
|
||||||
Content("text/plain", generateContent(citizen, target))
|
|
||||||
).apply {
|
|
||||||
addContent(Content("text/html", generateHtmlContent(citizen, target)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateHtmlContent(citizen: CitizenCreatorI, target: EntityI): String? {
|
|
||||||
return when (target) {
|
|
||||||
is ArticleWithTitleI -> """
|
|
||||||
Hello ${citizen.name.getFullName()},<br/>
|
|
||||||
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
|
|
||||||
""".trimIndent()
|
|
||||||
else -> noTarget(target.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateContent(citizen: CitizenCreatorI, target: EntityI): String {
|
|
||||||
return when (target) {
|
|
||||||
is ArticleWithTitleI -> """
|
|
||||||
Hello ${citizen.name.getFullName()},
|
|
||||||
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
|
|
||||||
""".trimIndent()
|
|
||||||
else -> noTarget(target.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoCitizen(message: String) : Exception(message)
|
|
||||||
class NoTarget(message: String) : Exception(message)
|
|
||||||
|
|
||||||
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
|
|
||||||
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
|
|
||||||
}
|
|
||||||
@@ -16,9 +16,9 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||||||
|
|
||||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
|
||||||
@JsonSubTypes(
|
@JsonSubTypes(
|
||||||
JsonSubTypes.Type(value = ArticleUpdateNotification::class, name = "article")
|
JsonSubTypes.Type(value = ArticleUpdateNotificationMessage::class, name = "article")
|
||||||
)
|
)
|
||||||
open class Notification(
|
open class NotificationMessage(
|
||||||
val type: String,
|
val type: String,
|
||||||
val createdAt: DateTime = DateTime.now()
|
val createdAt: DateTime = DateTime.now()
|
||||||
) {
|
) {
|
||||||
@@ -48,16 +48,16 @@ open class Notification(
|
|||||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Notification> fromString(raw: String): T = mapper.readValue(raw)
|
inline fun <reified T : NotificationMessage> fromString(raw: String): T = mapper.readValue(raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class EntityNotification(
|
open class EntityNotificationMessage <E : Entity> (
|
||||||
val target: Entity,
|
val target: E,
|
||||||
type: String,
|
type: String,
|
||||||
val action: String
|
val action: String
|
||||||
) : Notification(type)
|
) : NotificationMessage(type)
|
||||||
|
|
||||||
class ArticleUpdateNotification(
|
class ArticleUpdateNotificationMessage(
|
||||||
target: ArticleForView
|
target: ArticleForView
|
||||||
) : EntityNotification(target, "article", "update")
|
) : EntityNotificationMessage<ArticleForView>(target, "article", "update")
|
||||||
@@ -7,12 +7,15 @@ import kotlinx.coroutines.coroutineScope
|
|||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class Publisher(
|
class NotificationPublisherAsync(
|
||||||
private val factory: ConnectionFactory,
|
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,
|
private val exchangeName: String,
|
||||||
) {
|
) {
|
||||||
suspend fun <T : EntityNotification> publish(it: T): Deferred<Unit> = coroutineScope {
|
/**
|
||||||
|
* Publish a new notification message to RabbitMQ
|
||||||
|
*/
|
||||||
|
suspend fun <T : EntityNotificationMessage<*>> publishAsync(it: T): Deferred<Unit> = coroutineScope {
|
||||||
async {
|
async {
|
||||||
factory.newConnection().use { connection ->
|
factory.newConnection().use { connection ->
|
||||||
connection.createChannel().use { channel ->
|
connection.createChannel().use { channel ->
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<out TargetRef>) {
|
||||||
|
val citizen = citizenRepo.findById(follow.createdBy.id) ?: noCitizen(follow.createdBy.id)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the complete target entity by its ID according to its reference
|
||||||
|
*/
|
||||||
|
val target = when (follow.target.reference) {
|
||||||
|
"article" -> articleRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
|
||||||
|
"citizen" -> citizenRepo.findById(follow.target.id) ?: noTarget(follow.target.id)
|
||||||
|
else -> noTarget(follow.target.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find content of the email according to the target type
|
||||||
|
*/
|
||||||
|
val content = when (target) {
|
||||||
|
is ArticleForView -> ArticleNotificationEmailContent(citizen, target, domain)
|
||||||
|
is Citizen -> CitizenNotificationEmailContent(citizen, target, domain)
|
||||||
|
else -> noTargetTypeImplementation(follow.target.reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send email */
|
||||||
|
mailer.sendEmail {
|
||||||
|
Mail(
|
||||||
|
Email("notification@$domain"),
|
||||||
|
content.subject,
|
||||||
|
Email(citizen.email),
|
||||||
|
Content("text/plain", content.content)
|
||||||
|
).apply {
|
||||||
|
addContent(Content("text/html", content.contentHtml))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoCitizen(message: String) : Exception(message)
|
||||||
|
class NoTarget(message: String) : Exception(message)
|
||||||
|
class NoTargetTypeImplement(message: String) : Exception(message)
|
||||||
|
|
||||||
|
private fun noCitizen(id: UUID): Nothing = throw NoCitizen("No Citizen with this id : $id")
|
||||||
|
private fun noTarget(id: UUID): Nothing = throw NoTarget("No Target with this id : $id")
|
||||||
|
private fun noTargetTypeImplementation(type: String): Nothing = throw NoTargetTypeImplement("No Target type implemented: $type")
|
||||||
|
}
|
||||||
@@ -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()},<br/>
|
||||||
|
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val content
|
||||||
|
get() = run {
|
||||||
|
"""
|
||||||
|
Hello ${citizen.name.getFullName()},
|
||||||
|
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: <a href="http://$domain/citizens/${target.id}">here</a>
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val content
|
||||||
|
get() = run {
|
||||||
|
"""
|
||||||
|
Hello ${citizen.name.getFullName()},
|
||||||
|
The citizen "${target.name}" was new activity, check it here: http://$domain/citizens/${target.id}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package fr.dcproject.component.notification.email.content
|
||||||
|
|
||||||
|
interface NotificationEmailContent {
|
||||||
|
val subject: String
|
||||||
|
val content: String
|
||||||
|
val contentHtml: String
|
||||||
|
}
|
||||||
@@ -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<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
|
||||||
|
private val logger: Logger = LoggerFactory.getLogger(NotificationPushConsumer::class.qualifiedName)
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
/* Config Rabbit */
|
||||||
|
declareQueue(QUEUE_NAME, exchangeName)
|
||||||
|
consumeQueue(QUEUE_NAME) { message ->
|
||||||
|
redis.zadd(
|
||||||
|
"notification:${message.follow.createdBy.id}",
|
||||||
|
message.event.id,
|
||||||
|
message.rawMessage
|
||||||
|
)
|
||||||
|
logger.debug("Notification was transferred to the redis (follower: ${message.follow.createdBy.id})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val QUEUE_NAME = "push"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package fr.dcproject.component.notification
|
package fr.dcproject.component.notification.push
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException
|
import com.fasterxml.jackson.core.JsonProcessingException
|
||||||
import fr.dcproject.component.auth.citizen
|
import fr.dcproject.component.auth.citizen
|
||||||
import fr.dcproject.component.citizen.database.CitizenI
|
import fr.dcproject.component.citizen.database.CitizenI
|
||||||
|
import fr.dcproject.component.notification.NotificationMessage
|
||||||
import io.ktor.http.cio.websocket.Frame
|
import io.ktor.http.cio.websocket.Frame
|
||||||
import io.ktor.http.cio.websocket.Frame.Text
|
import io.ktor.http.cio.websocket.Frame.Text
|
||||||
import io.ktor.http.cio.websocket.readText
|
import io.ktor.http.cio.websocket.readText
|
||||||
@@ -28,31 +29,42 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class NotificationsPush(
|
/**
|
||||||
|
* Listen a custom flow to mark as read a message.
|
||||||
|
*
|
||||||
|
* And listen the redis subscription flow and call a callback when a new message arrives
|
||||||
|
*/
|
||||||
|
class NotificationPushListener(
|
||||||
private val redis: RedisAsyncCommands<String, String>,
|
private val redis: RedisAsyncCommands<String, String>,
|
||||||
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
|
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
|
||||||
citizen: CitizenI,
|
citizen: CitizenI,
|
||||||
incoming: Flow<Notification>,
|
incoming: Flow<NotificationMessage>,
|
||||||
onReceive: suspend (Notification) -> Unit,
|
onReceive: suspend (NotificationMessage) -> Unit,
|
||||||
) {
|
) {
|
||||||
class Builder(val redisClient: RedisClient) {
|
class Builder(redisClient: RedisClient) {
|
||||||
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
|
private val redisConnection = redisClient.connect()
|
||||||
private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis PubSub")
|
private val redisConnectionPubSub = redisClient.connectPubSub()
|
||||||
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis Async")
|
private val redis: RedisAsyncCommands<String, String> = redisConnection.async()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Listener with citizen, incoming flow and set an outgoing callback
|
||||||
|
*/
|
||||||
fun build(
|
fun build(
|
||||||
citizen: CitizenI,
|
citizen: CitizenI,
|
||||||
incoming: Flow<Notification>,
|
incoming: Flow<NotificationMessage>,
|
||||||
onReceive: suspend (Notification) -> Unit,
|
onReceive: suspend (NotificationMessage) -> Unit,
|
||||||
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onReceive)
|
): NotificationPushListener = NotificationPushListener(redis, redisConnectionPubSub, citizen, incoming, onReceive)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build NotificationPush with only a WebSocket session
|
||||||
|
*/
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
|
fun build(ws: DefaultWebSocketServerSession): NotificationPushListener {
|
||||||
/* Convert channel of string from websocket, to a flow of Notification object */
|
/* Convert channel of string from websocket, to a flow of Notification object */
|
||||||
val incomingFlow: Flow<Notification> = ws.incoming.consumeAsFlow()
|
val incomingFlow: Flow<NotificationMessage> = ws.incoming.consumeAsFlow()
|
||||||
.mapNotNull<Frame, Text> { it as? Frame.Text }
|
.mapNotNull<Frame, Text> { it as? Text }
|
||||||
.map { it.readText() }
|
.map { it.readText() }
|
||||||
.map { Notification.fromString(it) }
|
.map { NotificationMessage.fromString(it) }
|
||||||
|
|
||||||
return build(ws.call.citizen, incomingFlow) {
|
return build(ws.call.citizen, incomingFlow) {
|
||||||
ws.outgoing.send(Text(it.toString()))
|
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 val key = "notification:${citizen.id}"
|
||||||
private var score: Double = 0.0
|
/**
|
||||||
|
* The last score (a kind of sorted ids) of message
|
||||||
|
*/
|
||||||
|
private var lastScore: Double = 0.0
|
||||||
|
/**
|
||||||
|
* Configure the listener to listen all new notifications
|
||||||
|
*/
|
||||||
private val listener = object : RedisPubSubAdapter<String, String>() {
|
private val listener = object : RedisPubSubAdapter<String, String>() {
|
||||||
/* On new key publish */
|
/* On new key publish */
|
||||||
override fun message(pattern: String?, channel: String?, message: String?) {
|
override fun message(pattern: String?, channel: String?, message: String?) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getNotifications().collect {
|
getNewUnreadNotifications().collect {
|
||||||
onReceive(it)
|
onReceive(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init the listener and the callback
|
||||||
|
*/
|
||||||
init {
|
init {
|
||||||
/* Mark as read all incoming notifications */
|
/* Mark as read all incoming notifications */
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
incoming.collect {
|
incoming.collect {
|
||||||
markAsRead(it)
|
it.markAsRead()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get old notification and sent it to websocket */
|
/* Get old notification and sent it to websocket */
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getNotifications().collect {
|
getNewUnreadNotifications().collect {
|
||||||
onReceive(it)
|
onReceive(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,38 +119,55 @@ class NotificationsPush(
|
|||||||
addListener(listener)
|
addListener(listener)
|
||||||
|
|
||||||
/* Register to the events */
|
/* 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() {
|
fun close() {
|
||||||
redisConnectionPubSub.removeListener(listener)
|
redisConnectionPubSub.removeListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Return flow with all new notifications */
|
/**
|
||||||
private fun getNotifications() = flow<Notification> {
|
* 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<NotificationMessage> {
|
||||||
redis
|
redis
|
||||||
.zrangebyscoreWithScores(
|
.zrangebyscoreWithScores(
|
||||||
key,
|
key,
|
||||||
Range.from(
|
Range.from(
|
||||||
Boundary.excluding(score),
|
Boundary.excluding(lastScore),
|
||||||
Boundary.including(Double.POSITIVE_INFINITY)
|
Boundary.including(Double.POSITIVE_INFINITY)
|
||||||
),
|
),
|
||||||
Limit.from(100)
|
Limit.from(100)
|
||||||
)
|
)
|
||||||
.get().forEach {
|
.get().forEach {
|
||||||
emit(Notification.fromString(it.value))
|
/* Build message object from raw string and return it */
|
||||||
if (it.score > score) score = it.score
|
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 {
|
try {
|
||||||
redis.zremrangebyscore(
|
redis.zremrangebyscore(
|
||||||
key,
|
key,
|
||||||
Range.from(
|
Range.from(
|
||||||
Boundary.including(notificationMessage.id),
|
Boundary.including(id),
|
||||||
Boundary.including(notificationMessage.id)
|
Boundary.including(id)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: JsonProcessingException) {
|
} catch (e: JsonProcessingException) {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package fr.dcproject.component.notification.routes
|
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.locations.KtorExperimentalLocationsAPI
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.websocket.webSocket
|
import io.ktor.websocket.webSocket
|
||||||
@@ -13,8 +13,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|||||||
*/
|
*/
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) {
|
fun Route.notificationArticle(pushListenerBuilder: NotificationPushListener.Builder) {
|
||||||
webSocket("/notifications") {
|
webSocket("/notifications") {
|
||||||
pushBuilder.build(this)
|
pushListenerBuilder.build(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -955,13 +955,105 @@ paths:
|
|||||||
description: Return only http status 204 on success
|
description: Return only http status 204 on success
|
||||||
401:
|
401:
|
||||||
$ref: '#/components/responses/401'
|
$ref: '#/components/responses/401'
|
||||||
|
|
||||||
|
/citizens/{citizen}/follows:
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/citizen'
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- JWTAuth: [ ]
|
||||||
|
summary: Return Follows of citizen
|
||||||
|
tags:
|
||||||
|
- follow
|
||||||
|
- citizen
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Return follows
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FollowResponse'
|
||||||
|
404:
|
||||||
|
description: Citizen not exist
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/404'
|
||||||
|
post:
|
||||||
|
security:
|
||||||
|
- JWTAuth: []
|
||||||
|
summary: Follow citizen
|
||||||
|
description: Follow a citizen to receive notifications of his activity
|
||||||
|
tags:
|
||||||
|
- follow
|
||||||
|
- citizen
|
||||||
|
responses:
|
||||||
|
201:
|
||||||
|
description: Return only http status 201 on success
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
404:
|
||||||
|
description: Citizen not exist
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/404'
|
||||||
|
delete:
|
||||||
|
security:
|
||||||
|
- JWTAuth: [ ]
|
||||||
|
summary: Unfollow one citizen
|
||||||
|
tags:
|
||||||
|
- follow
|
||||||
|
- citizen
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: Return only http status 204 on success
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
404:
|
||||||
|
description: Citizen not exist
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/404'
|
||||||
|
/citizens/{citizen}/follows/citizens:
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/citizen'
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- JWTAuth: [ ]
|
||||||
|
summary: Return citizen Follow of citizen
|
||||||
|
tags:
|
||||||
|
- follow
|
||||||
|
- citizen
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Return citizen Follow of citizen
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/Paginated'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
result:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/FollowResponse'
|
||||||
|
404:
|
||||||
|
description: Citizen not exist
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/404'
|
||||||
|
|
||||||
/citizens/{citizen}/follows/articles:
|
/citizens/{citizen}/follows/articles:
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/citizen'
|
- $ref: '#/components/parameters/citizen'
|
||||||
get:
|
get:
|
||||||
security:
|
security:
|
||||||
- JWTAuth: [ ]
|
- JWTAuth: [ ]
|
||||||
summary: Return Follow or nothing if you not follow
|
summary: Return article Follow of citizen
|
||||||
tags:
|
tags:
|
||||||
- follow
|
- follow
|
||||||
- article
|
- article
|
||||||
@@ -1036,7 +1128,7 @@ paths:
|
|||||||
- citizen
|
- citizen
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Return your follows
|
description: Return constitution Follow of citizen
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
create or replace function find_follows_article_by_target(
|
create or replace function find_follows_article_by_target(
|
||||||
_target_id uuid,
|
_target_id uuid,
|
||||||
"limit" int default 50,
|
_limit int default 50,
|
||||||
"offset" int default 0,
|
_start_id uuid default null,
|
||||||
out resource json,
|
out resource json
|
||||||
out total int
|
|
||||||
) language plpgsql as
|
) language plpgsql as
|
||||||
$$
|
$$
|
||||||
declare
|
declare
|
||||||
_version_id uuid = (select version_id from article where id = _target_id);
|
_version_id uuid = (select version_id from article where id = _target_id);
|
||||||
|
_start_at timestamp default '2000-01-01 00:00:00'::timestamp;
|
||||||
|
_article_creator_id uuid = (select created_by_id from article where id = _target_id);
|
||||||
begin
|
begin
|
||||||
select json_agg(t), (
|
if _start_id is not null then
|
||||||
select count(f.id)
|
select created_at into _start_at from follow where id = _start_id;
|
||||||
from follow f
|
end if;
|
||||||
join article a on f.target_id = a.id
|
|
||||||
where a.version_id = _version_id)
|
select json_agg(t)
|
||||||
into resource, total
|
into resource
|
||||||
from (
|
from (
|
||||||
select
|
select
|
||||||
f.id,
|
f.id,
|
||||||
@@ -22,11 +23,17 @@ begin
|
|||||||
f.target_reference,
|
f.target_reference,
|
||||||
json_build_object('id', f.target_id) as target,
|
json_build_object('id', f.target_id) as target,
|
||||||
find_citizen_by_id_with_user(f.created_by_id) as created_by
|
find_citizen_by_id_with_user(f.created_by_id) as created_by
|
||||||
from follow_article as f
|
from follow as f
|
||||||
join article a on f.target_id = a.id
|
left join article a on f.target_reference = 'article'::regclass and f.target_id = a.id
|
||||||
where a.version_id = _version_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
|
order by f.created_at
|
||||||
limit "limit" offset "offset"
|
limit _limit
|
||||||
) as t;
|
) as t;
|
||||||
end
|
end
|
||||||
$$;
|
$$;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
$$;
|
||||||
@@ -10,10 +10,11 @@ import fr.dcproject.component.citizen.database.CitizenCreator
|
|||||||
import fr.dcproject.component.citizen.database.CitizenI
|
import fr.dcproject.component.citizen.database.CitizenI
|
||||||
import fr.dcproject.component.follow.database.FollowArticleRepository
|
import fr.dcproject.component.follow.database.FollowArticleRepository
|
||||||
import fr.dcproject.component.follow.database.FollowForView
|
import fr.dcproject.component.follow.database.FollowForView
|
||||||
import fr.dcproject.component.notification.ArticleUpdateNotification
|
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage
|
||||||
import fr.dcproject.component.notification.NotificationConsumer
|
import fr.dcproject.component.notification.NotificationPublisherAsync
|
||||||
import fr.dcproject.component.notification.NotificationEmailSender
|
import fr.dcproject.component.notification.email.NotificationEmailConsumer
|
||||||
import fr.dcproject.component.notification.Publisher
|
import fr.dcproject.component.notification.email.NotificationEmailSender
|
||||||
|
import fr.dcproject.component.notification.push.NotificationPushConsumer
|
||||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
import io.lettuce.core.RedisClient
|
import io.lettuce.core.RedisClient
|
||||||
@@ -65,7 +66,7 @@ class NotificationConsumerTest {
|
|||||||
@KtorExperimentalAPI
|
@KtorExperimentalAPI
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@Test
|
@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")
|
val config: Configuration = Configuration("application-test.conf")
|
||||||
/* Create mocks and spy's */
|
/* Create mocks and spy's */
|
||||||
val emailSender = mockk<NotificationEmailSender>() {
|
val emailSender = mockk<NotificationEmailSender>() {
|
||||||
@@ -88,21 +89,30 @@ class NotificationConsumerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Config consumer */
|
/* Config consumer */
|
||||||
val consumer = NotificationConsumer(
|
val emailConsumer = NotificationEmailConsumer(
|
||||||
rabbitFactory = rabbitFactory,
|
rabbitFactory = rabbitFactory,
|
||||||
redisClient = redisClient,
|
|
||||||
followArticleRepo = followArticleRepo,
|
followArticleRepo = followArticleRepo,
|
||||||
followConstitutionRepo = mockk(),
|
followConstitutionRepo = mockk(), // TODO test followConstitution
|
||||||
|
followCitizenRepo = mockk(), // TODO test followCitizen
|
||||||
notificationEmailSender = emailSender,
|
notificationEmailSender = emailSender,
|
||||||
exchangeName = "notification",
|
exchangeName = "notification",
|
||||||
).apply { start() }
|
).apply { start() }
|
||||||
|
|
||||||
|
val pushConsumer = NotificationPushConsumer(
|
||||||
|
rabbitFactory = rabbitFactory,
|
||||||
|
followArticleRepo = followArticleRepo,
|
||||||
|
followConstitutionRepo = mockk(), // TODO test followConstitution
|
||||||
|
followCitizenRepo = mockk(), // TODO test followCitizen
|
||||||
|
redisClient = redisClient,
|
||||||
|
exchangeName = "notification",
|
||||||
|
).apply { start() }
|
||||||
|
|
||||||
/* Push message */
|
/* Push message */
|
||||||
Publisher(
|
NotificationPublisherAsync(
|
||||||
factory = rabbitFactory,
|
factory = rabbitFactory,
|
||||||
exchangeName = "notification",
|
exchangeName = "notification",
|
||||||
).publish(
|
).publishAsync(
|
||||||
ArticleUpdateNotification(
|
ArticleUpdateNotificationMessage(
|
||||||
ArticleForView(
|
ArticleForView(
|
||||||
title = "MyTitle",
|
title = "MyTitle",
|
||||||
content = "myContent",
|
content = "myContent",
|
||||||
@@ -121,6 +131,7 @@ class NotificationConsumerTest {
|
|||||||
verify(timeout = 2000) { emailSender.sendEmail(any()) }
|
verify(timeout = 2000) { emailSender.sendEmail(any()) }
|
||||||
verify(timeout = 2000) { asyncCommand.zadd(any<String>(), any<Double>(), any<String>()) }
|
verify(timeout = 2000) { asyncCommand.zadd(any<String>(), any<Double>(), any<String>()) }
|
||||||
|
|
||||||
consumer.close()
|
emailConsumer.close()
|
||||||
|
pushConsumer.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import fr.dcproject.component.article.database.ArticleForView
|
|||||||
import fr.dcproject.component.auth.database.UserCreator
|
import fr.dcproject.component.auth.database.UserCreator
|
||||||
import fr.dcproject.component.citizen.database.CitizenCreator
|
import fr.dcproject.component.citizen.database.CitizenCreator
|
||||||
import fr.dcproject.component.citizen.database.CitizenI
|
import fr.dcproject.component.citizen.database.CitizenI
|
||||||
import fr.dcproject.component.notification.ArticleUpdateNotification
|
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage
|
||||||
import fr.dcproject.component.notification.Notification
|
import fr.dcproject.component.notification.NotificationMessage
|
||||||
import fr.dcproject.component.notification.NotificationsPush
|
import fr.dcproject.component.notification.push.NotificationPushListener
|
||||||
import io.lettuce.core.RedisClient
|
import io.lettuce.core.RedisClient
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
@@ -68,14 +68,14 @@ internal class NotificationsPushTest {
|
|||||||
title = "Super Title",
|
title = "Super Title",
|
||||||
)
|
)
|
||||||
/* Init two notification, one called before subscription, and the other after */
|
/* Init two notification, one called before subscription, and the other after */
|
||||||
val notifBeforeSubscribe = ArticleUpdateNotification(article)
|
val notifBeforeSubscribe = ArticleUpdateNotificationMessage(article)
|
||||||
runBlocking {
|
runBlocking {
|
||||||
delay(100)
|
delay(100)
|
||||||
}
|
}
|
||||||
val notifAfterSubscribe = ArticleUpdateNotification(article)
|
val notifAfterSubscribe = ArticleUpdateNotificationMessage(article)
|
||||||
|
|
||||||
/* init event for emulate incoming message from websocket */
|
/* init event for emulate incoming message from websocket */
|
||||||
val event = MutableSharedFlow<Notification>()
|
val event = MutableSharedFlow<NotificationMessage>()
|
||||||
val incomingFlow = event.asSharedFlow()
|
val incomingFlow = event.asSharedFlow()
|
||||||
|
|
||||||
spyk(object { var counter = 0 }).run { /* Counter for count the callback of notification */
|
spyk(object { var counter = 0 }).run { /* Counter for count the callback of notification */
|
||||||
@@ -90,7 +90,7 @@ internal class NotificationsPushTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Init NotificationPush system, and set assertion in callback */
|
/* Init NotificationPush system, and set assertion in callback */
|
||||||
val notificationPush = NotificationsPush.Builder(redisClient).build(citizen, incomingFlow) {
|
val notificationPush = NotificationPushListener.Builder(redisClient).build(citizen, incomingFlow) {
|
||||||
counter++
|
counter++
|
||||||
if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id
|
if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id
|
||||||
else it.id `should be equal to` notifAfterSubscribe.id
|
else it.id `should be equal to` notifAfterSubscribe.id
|
||||||
|
|||||||
96
src/test/kotlin/integration/Follow citizen routes.kt
Normal file
96
src/test/kotlin/integration/Follow citizen routes.kt
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,20 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import fr.dcproject.common.utils.toUUID
|
|
||||||
import fr.dcproject.component.article.database.ArticleForView
|
|
||||||
import fr.dcproject.component.auth.database.UserCreator
|
|
||||||
import fr.dcproject.component.citizen.database.CitizenCreator
|
|
||||||
import fr.dcproject.component.citizen.database.CitizenI.Name
|
import fr.dcproject.component.citizen.database.CitizenI.Name
|
||||||
import fr.dcproject.component.notification.ArticleUpdateNotification
|
import fr.dcproject.component.notification.ArticleUpdateNotificationMessage
|
||||||
import fr.dcproject.component.notification.Notification
|
import fr.dcproject.component.notification.NotificationMessage
|
||||||
import fr.dcproject.component.notification.Publisher
|
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 article`
|
||||||
import integration.steps.given.`Given I have citizen`
|
import integration.steps.given.`Given I have citizen`
|
||||||
import integration.steps.given.`Given I have follow on article`
|
import integration.steps.given.`Given I have follow on article`
|
||||||
import integration.steps.given.`authenticated in url as`
|
import integration.steps.given.`authenticated in url as`
|
||||||
import io.ktor.http.cio.websocket.Frame
|
import io.ktor.http.cio.websocket.Frame
|
||||||
import io.ktor.http.cio.websocket.readText
|
import io.ktor.http.cio.websocket.readText
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
import org.junit.jupiter.api.Tags
|
import org.junit.jupiter.api.Tags
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.koin.test.get
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@@ -31,26 +26,7 @@ class `Notification routes` : BaseTest() {
|
|||||||
`Given I have citizen`("John", "Doe", id = "1a34191a-9cde-45ba-8ac1-230138a102d3")
|
`Given I have citizen`("John", "Doe", id = "1a34191a-9cde-45ba-8ac1-230138a102d3")
|
||||||
`Given I have article`(id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", createdBy = Name(firstName = "John", lastName = "Doe"))
|
`Given I have article`(id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", createdBy = Name(firstName = "John", lastName = "Doe"))
|
||||||
`Given I have follow on article`("John", "Doe", article = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4")
|
`Given I have follow on article`("John", "Doe", article = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4")
|
||||||
val notification = ArticleUpdateNotification(
|
`Given I have article update notification`("a06cbfb7-3094-4d64-aaa1-7486c0c292f4")
|
||||||
ArticleForView(
|
|
||||||
id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4".toUUID(),
|
|
||||||
title = "MyTitle",
|
|
||||||
content = "myContent",
|
|
||||||
description = "myDescription",
|
|
||||||
createdBy = CitizenCreator(
|
|
||||||
id = "1a34191a-9cde-45ba-8ac1-230138a102d3".toUUID(),
|
|
||||||
name = Name(firstName = "John", lastName = "Doe"),
|
|
||||||
email = "john-doe@plop.com",
|
|
||||||
user = UserCreator(username = "john-doe"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val publisher = get<Publisher>()
|
|
||||||
launch {
|
|
||||||
publisher
|
|
||||||
.publish(notification)
|
|
||||||
.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.sleep(1000)
|
Thread.sleep(1000)
|
||||||
|
|
||||||
@@ -62,7 +38,41 @@ class `Notification routes` : BaseTest() {
|
|||||||
) { incoming, outgoing ->
|
) { incoming, outgoing ->
|
||||||
incoming.receive().let {
|
incoming.receive().let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Frame.Text -> Notification.fromString<ArticleUpdateNotification>(it.readText()).let { notif ->
|
is Frame.Text -> NotificationMessage.fromString<ArticleUpdateNotificationMessage>(it.readText()).let { notif ->
|
||||||
|
assertEquals(
|
||||||
|
"a06cbfb7-3094-4d64-aaa1-7486c0c292f4",
|
||||||
|
notif.target.id.toString()
|
||||||
|
)
|
||||||
|
outgoing.send(it)
|
||||||
|
}
|
||||||
|
else -> error(it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `I can receive article update notification when follow the creator`() {
|
||||||
|
withIntegrationApplication {
|
||||||
|
`Given I have citizen`("Thomas", "Pesquet", id = "1a34191a-9cde-45ba-8ac1-230138a102d3")
|
||||||
|
`Given I have article`(id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", createdBy = Name(firstName = "Thomas", lastName = "Pesquet"))
|
||||||
|
`Given I have citizen`("Alan", "Bean") {
|
||||||
|
`And follow citizen`(Name("Thomas", "Pesquet"))
|
||||||
|
}
|
||||||
|
`Given I have article update notification`("a06cbfb7-3094-4d64-aaa1-7486c0c292f4")
|
||||||
|
|
||||||
|
Thread.sleep(1000)
|
||||||
|
|
||||||
|
handleWebSocketConversation(
|
||||||
|
"/notifications",
|
||||||
|
{
|
||||||
|
`authenticated in url as`("Alan", "Bean")
|
||||||
|
}
|
||||||
|
) { incoming, outgoing ->
|
||||||
|
incoming.receive().let {
|
||||||
|
when (it) {
|
||||||
|
is Frame.Text -> NotificationMessage.fromString<ArticleUpdateNotificationMessage>(it.readText()).let { notif ->
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"a06cbfb7-3094-4d64-aaa1-7486c0c292f4",
|
"a06cbfb7-3094-4d64-aaa1-7486c0c292f4",
|
||||||
notif.target.id.toString()
|
notif.target.id.toString()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import fr.dcproject.component.citizen.database.CitizenRef
|
|||||||
import fr.dcproject.component.citizen.database.CitizenRepository
|
import fr.dcproject.component.citizen.database.CitizenRepository
|
||||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||||
import fr.dcproject.component.follow.database.FollowArticleRepository
|
import fr.dcproject.component.follow.database.FollowArticleRepository
|
||||||
|
import fr.dcproject.component.follow.database.FollowCitizenRepository
|
||||||
import fr.dcproject.component.follow.database.FollowConstitutionRepository
|
import fr.dcproject.component.follow.database.FollowConstitutionRepository
|
||||||
import fr.dcproject.component.follow.database.FollowForUpdate
|
import fr.dcproject.component.follow.database.FollowForUpdate
|
||||||
import io.ktor.server.testing.TestApplicationEngine
|
import io.ktor.server.testing.TestApplicationEngine
|
||||||
@@ -24,6 +25,18 @@ fun Citizen.`And follow constitution`(
|
|||||||
) {
|
) {
|
||||||
createFollow(this, ConstitutionRef(constitution.toUUID()))
|
createFollow(this, ConstitutionRef(constitution.toUUID()))
|
||||||
}
|
}
|
||||||
|
fun Citizen.`And follow citizen`(
|
||||||
|
citizen: String,
|
||||||
|
) {
|
||||||
|
createFollow(this, CitizenRef(citizen.toUUID()))
|
||||||
|
}
|
||||||
|
fun Citizen.`And follow citizen`(
|
||||||
|
name: CitizenI.Name,
|
||||||
|
) {
|
||||||
|
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() }
|
||||||
|
val citizen = citizenRepository.findByName(name) ?: error("Citizen not exist")
|
||||||
|
createFollow(this, CitizenRef(citizen.id))
|
||||||
|
}
|
||||||
|
|
||||||
fun TestApplicationEngine.`Given I have follow on article`(
|
fun TestApplicationEngine.`Given I have follow on article`(
|
||||||
firstName: String,
|
firstName: String,
|
||||||
@@ -35,6 +48,17 @@ fun TestApplicationEngine.`Given I have follow on article`(
|
|||||||
createFollow(citizen, ArticleRef(article.toUUID()))
|
createFollow(citizen, ArticleRef(article.toUUID()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TestApplicationEngine.`Given I have follow on citizen`(
|
||||||
|
firstName: String,
|
||||||
|
lastName: String,
|
||||||
|
target: CitizenI.Name,
|
||||||
|
) {
|
||||||
|
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().get() }
|
||||||
|
val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist")
|
||||||
|
val targetCitizen = citizenRepository.findByName(target) ?: error("Citizen not exist")
|
||||||
|
createFollow(citizen, CitizenRef(targetCitizen.id))
|
||||||
|
}
|
||||||
|
|
||||||
fun TestApplicationEngine.`Given I have follow on constitution`(
|
fun TestApplicationEngine.`Given I have follow on constitution`(
|
||||||
firstName: String,
|
firstName: String,
|
||||||
lastName: String,
|
lastName: String,
|
||||||
@@ -56,3 +80,9 @@ fun createFollow(citizen: CitizenRef, constitution: ConstitutionRef) {
|
|||||||
val follow = FollowForUpdate(createdBy = citizen, target = constitution)
|
val follow = FollowForUpdate(createdBy = citizen, target = constitution)
|
||||||
followConstitutionRepository.follow(follow)
|
followConstitutionRepository.follow(follow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createFollow(createdBy: CitizenRef, target: CitizenRef) {
|
||||||
|
val followCitizenRepository: FollowCitizenRepository by lazy { GlobalContext.get().get() }
|
||||||
|
val follow = FollowForUpdate(createdBy = createdBy, target = target)
|
||||||
|
followCitizenRepository.follow(follow)
|
||||||
|
}
|
||||||
|
|||||||
40
src/test/kotlin/integration/steps/given/Notification.kt
Normal file
40
src/test/kotlin/integration/steps/given/Notification.kt
Normal file
@@ -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<NotificationPublisherAsync>()
|
||||||
|
.publishAsync(notification)
|
||||||
|
.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,17 +7,19 @@ declare
|
|||||||
_version_id1 uuid = uuid_generate_v4();
|
_version_id1 uuid = uuid_generate_v4();
|
||||||
first_article_id uuid := fixture_article(_citizen_id := _citizen_id, _version_id := _version_id1);
|
first_article_id uuid := fixture_article(_citizen_id := _citizen_id, _version_id := _version_id1);
|
||||||
first_article_updated_id uuid;
|
first_article_updated_id uuid;
|
||||||
|
_follow_count int = 0;
|
||||||
begin
|
begin
|
||||||
perform follow('citizen'::regclass, _citizen_id, _citizen_id2);
|
perform follow('citizen'::regclass, _citizen_id, _citizen_id2);
|
||||||
assert (select count(*) = 1 from follow), 'follow must be inserted';
|
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';
|
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);
|
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);
|
perform unfollow('citizen'::regclass, _citizen_id, _citizen_id2);
|
||||||
assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow';
|
assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow';
|
||||||
|
|
||||||
|
|
||||||
perform follow('article'::regclass, first_article_id, _citizen_id);
|
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 = 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';
|
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_id, _citizen_id, 'article')), '(v1) find_follow must return the following';
|
||||||
assert (select following = true from find_follow(first_article_updated_id, _citizen_id, 'article')), '(v2) find_follow must return the following';
|
assert (select following = true from find_follow(first_article_updated_id, _citizen_id, 'article')), '(v2) find_follow must return the following';
|
||||||
|
|
||||||
assert (select f.total = 1 from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return 1 follow';
|
assert (select count(*) = 1 from follow), 'must be only 1 follow';
|
||||||
assert (select (f.resource#>>'{0, created_by, id}')::uuid = _citizen_id from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return follows with creator';
|
perform 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);
|
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;
|
rollback;
|
||||||
raise notice 'follow test pass';
|
raise notice 'follow test pass';
|
||||||
|
|||||||
Reference in New Issue
Block a user