cleanup and refactoring of notification
close rabbit and redis connexion on application close Refactoring of Configuration class fix notification id increment Add builder for NotificationPush Add close to notificationPush to remove listener Clean tags of tests purge queue before functional tests
This commit is contained in:
2
.idea/runConfigurations/Unit_Tests.xml
generated
2
.idea/runConfigurations/Unit_Tests.xml
generated
@@ -15,7 +15,7 @@
|
|||||||
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
|
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
|
||||||
</envs>
|
</envs>
|
||||||
<dir value="$PROJECT_DIR$" />
|
<dir value="$PROJECT_DIR$" />
|
||||||
<tag value="!functional" />
|
<tag value="unit" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
23
.idea/runConfigurations/Unit_Tests__offline_.xml
generated
23
.idea/runConfigurations/Unit_Tests__offline_.xml
generated
@@ -1,23 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Unit Tests (offline)" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
|
|
||||||
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
|
|
||||||
<useClassPathOnly />
|
|
||||||
<option name="PACKAGE_NAME" value="fr.dcproject" />
|
|
||||||
<option name="MAIN_CLASS_NAME" value="" />
|
|
||||||
<option name="METHOD_NAME" value="" />
|
|
||||||
<option name="TEST_OBJECT" value="tags" />
|
|
||||||
<option name="VM_PARAMETERS" value="-ea -Dcucumber.options="--tags ~@online" -Djdk.attach.allowAttachSelf=true" />
|
|
||||||
<option name="PARAMETERS" value="" />
|
|
||||||
<option name="TEST_SEARCH_SCOPE">
|
|
||||||
<value defaultName="wholeProject" />
|
|
||||||
</option>
|
|
||||||
<envs>
|
|
||||||
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
|
|
||||||
</envs>
|
|
||||||
<dir value="$PROJECT_DIR$" />
|
|
||||||
<tag value="!online" />
|
|
||||||
<method v="2">
|
|
||||||
<option name="Make" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
@@ -39,6 +39,7 @@ import fr.dcproject.routes.notificationArticle
|
|||||||
import fr.dcproject.security.AccessDeniedException
|
import fr.dcproject.security.AccessDeniedException
|
||||||
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.call
|
import io.ktor.application.call
|
||||||
import io.ktor.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.auth.Authentication
|
import io.ktor.auth.Authentication
|
||||||
@@ -122,7 +123,12 @@ fun Application.module(env: Env = PROD) {
|
|||||||
masking = false
|
masking = false
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationConsumer(get(), get(), get(), get(), get(), Configuration.exchangeNotificationName).config()
|
get<NotificationConsumer>().run {
|
||||||
|
start()
|
||||||
|
environment.monitor.subscribe(ApplicationStopped) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
install(Authentication, jwtInstallation(get()))
|
install(Authentication, jwtInstallation(get()))
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,39 @@
|
|||||||
package fr.dcproject.application
|
package fr.dcproject.application
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object Configuration {
|
class Configuration(val config: Config) {
|
||||||
private var config = ConfigFactory.load()
|
constructor(resourceBasename: String? = null) : this(if (resourceBasename == null) ConfigFactory.load() else ConfigFactory.load(resourceBasename))
|
||||||
|
|
||||||
object Sql {
|
interface Sql {
|
||||||
val migrationFiles: URI = this::class.java.getResource("/sql/migrations")?.toURI() ?: error("No migrations found")
|
val migrationFiles: URI
|
||||||
val functionFiles: URI = this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found")
|
val functionFiles: URI
|
||||||
val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures")?.toURI() ?: error("No sql fixture found")
|
val fixtureFiles: URI
|
||||||
}
|
}
|
||||||
object Database {
|
val sql
|
||||||
val host: String = config.getString("db.host")
|
get() = object : Sql {
|
||||||
val port: Int = config.getInt("db.port")
|
override val migrationFiles: URI = this::class.java.getResource("/sql/migrations")?.toURI() ?: error("No migrations found")
|
||||||
var database: String = config.getString("db.database")
|
override val functionFiles: URI = this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found")
|
||||||
var username: String = config.getString("db.username")
|
override val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures")?.toURI() ?: error("No sql fixture found")
|
||||||
var password: String = config.getString("db.password")
|
}
|
||||||
|
|
||||||
|
interface Database {
|
||||||
|
val host: String
|
||||||
|
val port: Int
|
||||||
|
var database: String
|
||||||
|
var username: String
|
||||||
|
var password: String
|
||||||
}
|
}
|
||||||
|
val database
|
||||||
|
get() = object : Database {
|
||||||
|
override val host: String = config.getString("db.host")
|
||||||
|
override val port: Int = config.getInt("db.port")
|
||||||
|
override var database: String = config.getString("db.database")
|
||||||
|
override var username: String = config.getString("db.username")
|
||||||
|
override var password: String = config.getString("db.password")
|
||||||
|
}
|
||||||
|
|
||||||
val envName: String = config.getString("app.envName")
|
val envName: String = config.getString("app.envName")
|
||||||
val domain: String = config.getString("app.domain")
|
val domain: String = config.getString("app.domain")
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import com.fasterxml.jackson.databind.module.SimpleModule
|
|||||||
import com.fasterxml.jackson.datatype.joda.JodaModule
|
import com.fasterxml.jackson.datatype.joda.JodaModule
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.rabbitmq.client.ConnectionFactory
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import fr.dcproject.notification.publisher.Publisher
|
|
||||||
import fr.dcproject.messages.Mailer
|
import fr.dcproject.messages.Mailer
|
||||||
import fr.dcproject.messages.NotificationEmailSender
|
import fr.dcproject.messages.NotificationEmailSender
|
||||||
|
import fr.dcproject.notification.NotificationConsumer
|
||||||
|
import fr.dcproject.notification.publisher.Publisher
|
||||||
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
|
||||||
@@ -18,36 +19,52 @@ import io.ktor.client.HttpClient
|
|||||||
import io.ktor.client.features.websocket.WebSockets
|
import io.ktor.client.features.websocket.WebSockets
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
import io.lettuce.core.RedisClient
|
import io.lettuce.core.RedisClient
|
||||||
import io.lettuce.core.api.async.RedisAsyncCommands
|
import notification.NotificationsPush
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
@KtorExperimentalAPI
|
@KtorExperimentalAPI
|
||||||
val KoinModule = module {
|
val KoinModule = module {
|
||||||
|
single { Configuration() }
|
||||||
|
|
||||||
// SQL connection
|
// SQL connection
|
||||||
single {
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
Connection(
|
Connection(
|
||||||
host = Configuration.Database.host,
|
host = config.database.host,
|
||||||
port = Configuration.Database.port,
|
port = config.database.port,
|
||||||
database = Configuration.Database.database,
|
database = config.database.database,
|
||||||
username = Configuration.Database.username,
|
username = config.database.username,
|
||||||
password = Configuration.Database.password
|
password = config.database.password
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch Database migration
|
// Launch Database migration
|
||||||
single { Migrations(get(), Configuration.Sql.migrationFiles, Configuration.Sql.functionFiles) }
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
|
Migrations(get(), config.sql.migrationFiles, config.sql.functionFiles)
|
||||||
|
}
|
||||||
|
|
||||||
// Redis client
|
// Redis client
|
||||||
single<RedisClient> {
|
single<RedisClient> {
|
||||||
RedisClient.create(Configuration.redis).apply {
|
val config: Configuration = get()
|
||||||
|
RedisClient.create(config.redis).apply {
|
||||||
connect().sync().configSet("notify-keyspace-events", "KEA")
|
connect().sync().configSet("notify-keyspace-events", "KEA")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single { NotificationsPush.Builder(get()) }
|
||||||
|
|
||||||
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
|
NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
|
||||||
|
}
|
||||||
|
|
||||||
// RabbitMQ
|
// RabbitMQ
|
||||||
single<ConnectionFactory> {
|
single<ConnectionFactory> {
|
||||||
ConnectionFactory().apply { setUri(Configuration.rabbitmq) }
|
val config: Configuration = get()
|
||||||
|
ConnectionFactory().apply { setUri(config.rabbitmq) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// JsonSerializer
|
// JsonSerializer
|
||||||
@@ -71,16 +88,26 @@ val KoinModule = module {
|
|||||||
|
|
||||||
// SQL Requester (postgresJson)
|
// SQL Requester (postgresJson)
|
||||||
single {
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
Requester.RequesterFactory(
|
Requester.RequesterFactory(
|
||||||
connection = get(),
|
connection = get(),
|
||||||
functionsDirectory = Configuration.Sql.functionFiles
|
functionsDirectory = config.sql.functionFiles
|
||||||
).createRequester()
|
).createRequester()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mailer
|
// Mailer
|
||||||
single { Mailer(Configuration.sendGridKey) }
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
|
Mailer(config.sendGridKey)
|
||||||
|
}
|
||||||
|
|
||||||
single { Publisher(factory = get(), exchangeName = Configuration.exchangeNotificationName) }
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
|
Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
|
||||||
|
}
|
||||||
|
|
||||||
single { NotificationEmailSender(get<Mailer>(), Configuration.domain, get(), get()) }
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
|
NotificationEmailSender(get<Mailer>(), config.domain, get(), get())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,8 @@ import org.koin.dsl.module
|
|||||||
val authKoinModule = module {
|
val authKoinModule = module {
|
||||||
single { UserRepository(get()) }
|
single { UserRepository(get()) }
|
||||||
// Used to send a connexion link by email
|
// Used to send a connexion link by email
|
||||||
single { PasswordlessAuth(get<Mailer>(), Configuration.domain, get()) }
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
|
PasswordlessAuth(get<Mailer>(), config.domain, get())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ import org.elasticsearch.client.RestClient
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val viewKoinModule = module {
|
val viewKoinModule = module {
|
||||||
// Elasticsearch Client
|
|
||||||
val esClient = RestClient.builder(
|
|
||||||
HttpHost.create(Configuration.elasticsearch)
|
|
||||||
).build().apply {
|
|
||||||
createEsIndexForViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
single { ArticleViewManager<ArticleForView>(esClient) }
|
single {
|
||||||
|
val config: Configuration = get()
|
||||||
|
// Elasticsearch Client
|
||||||
|
val esClient = RestClient.builder(
|
||||||
|
HttpHost.create(config.elasticsearch)
|
||||||
|
).build().apply {
|
||||||
|
createEsIndexForViews()
|
||||||
|
}
|
||||||
|
ArticleViewManager<ArticleForView>(esClient)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
|||||||
import fr.dcproject.component.article.ArticleForView
|
import fr.dcproject.component.article.ArticleForView
|
||||||
import fr.postgresjson.entity.UuidEntity
|
import fr.postgresjson.entity.UuidEntity
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import kotlin.random.Random
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
open class Notification(
|
open class Notification(
|
||||||
val type: String,
|
val type: String,
|
||||||
val createdAt: DateTime = DateTime.now()
|
val createdAt: DateTime = DateTime.now()
|
||||||
) {
|
) {
|
||||||
val id: Double = randId(createdAt.millis)
|
val id: Double = nextId()
|
||||||
|
|
||||||
private fun randId(time: Long): Double {
|
private fun nextId(): Double {
|
||||||
return (time.toString() + Random.nextInt(1000, 9999).toString()).toDouble()
|
return (createdAt.millis.toString() + nextInt().toString()).toDouble()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = mapper.writeValueAsString(this) ?: error("Unable to serialize notification")
|
override fun toString(): String = mapper.writeValueAsString(this) ?: error("Unable to serialize notification")
|
||||||
@@ -27,6 +27,12 @@ open class Notification(
|
|||||||
fun toByteArray() = toString().toByteArray()
|
fun toByteArray() = toString().toByteArray()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val counter: AtomicInteger = AtomicInteger(1000)
|
||||||
|
fun nextInt(): Int {
|
||||||
|
counter.compareAndSet(9999, 1000)
|
||||||
|
return counter.incrementAndGet()
|
||||||
|
}
|
||||||
|
|
||||||
val mapper = jacksonObjectMapper().apply {
|
val mapper = jacksonObjectMapper().apply {
|
||||||
registerModule(SimpleModule())
|
registerModule(SimpleModule())
|
||||||
propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
|
propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
|
||||||
|
|||||||
@@ -29,10 +29,18 @@ class NotificationConsumer(
|
|||||||
private val notificationEmailSender: NotificationEmailSender,
|
private val notificationEmailSender: NotificationEmailSender,
|
||||||
private val exchangeName: String,
|
private val exchangeName: String,
|
||||||
) {
|
) {
|
||||||
val redis: RedisAsyncCommands<String, String> = redisClient.connect()?.async() ?: error("Unable to connect to redis")
|
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)
|
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
|
||||||
|
|
||||||
fun config() {
|
fun close() {
|
||||||
|
rabbitChannel.close()
|
||||||
|
rabbitConnection.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
/* Config Rabbit */
|
/* Config Rabbit */
|
||||||
rabbitFactory.newConnection().use { connection ->
|
rabbitFactory.newConnection().use { connection ->
|
||||||
connection.createChannel().use { channel ->
|
connection.createChannel().use { channel ->
|
||||||
@@ -45,8 +53,6 @@ class NotificationConsumer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Define Consumer */
|
/* Define Consumer */
|
||||||
val rabbitChannel = rabbitFactory.newConnection().createChannel()
|
|
||||||
|
|
||||||
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
|
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun handleDelivery(
|
override fun handleDelivery(
|
||||||
@@ -106,4 +112,4 @@ class NotificationConsumer(
|
|||||||
val rawMessage: String,
|
val rawMessage: String,
|
||||||
val follow: FollowSimple<out TargetRef, CitizenRef>
|
val follow: FollowSimple<out TargetRef, CitizenRef>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,80 @@
|
|||||||
package notification
|
package notification
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException
|
import com.fasterxml.jackson.core.JsonProcessingException
|
||||||
|
import fr.dcproject.component.auth.citizen
|
||||||
import fr.dcproject.component.citizen.CitizenI
|
import fr.dcproject.component.citizen.CitizenI
|
||||||
import fr.dcproject.notification.Notification
|
import fr.dcproject.notification.Notification
|
||||||
|
import io.ktor.http.cio.websocket.Frame
|
||||||
|
import io.ktor.http.cio.websocket.Frame.Text
|
||||||
|
import io.ktor.http.cio.websocket.readText
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.websocket.DefaultWebSocketServerSession
|
||||||
import io.lettuce.core.Limit
|
import io.lettuce.core.Limit
|
||||||
import io.lettuce.core.Range
|
import io.lettuce.core.Range
|
||||||
import io.lettuce.core.Range.Boundary
|
import io.lettuce.core.Range.Boundary
|
||||||
import io.lettuce.core.RedisClient
|
import io.lettuce.core.RedisClient
|
||||||
import io.lettuce.core.api.async.RedisAsyncCommands
|
import io.lettuce.core.api.async.RedisAsyncCommands
|
||||||
import io.lettuce.core.pubsub.RedisPubSubAdapter
|
import io.lettuce.core.pubsub.RedisPubSubAdapter
|
||||||
|
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class NotificationsPush (
|
class NotificationsPush private constructor(
|
||||||
val redisClient: RedisClient,
|
private val redis: RedisAsyncCommands<String, String>,
|
||||||
|
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
|
||||||
citizen: CitizenI,
|
citizen: CitizenI,
|
||||||
incoming: Flow<Notification>,
|
incoming: Flow<Notification>,
|
||||||
onRecieve: suspend (Notification) -> Unit,
|
onRecieve: suspend (Notification) -> Unit,
|
||||||
)
|
) {
|
||||||
{
|
class Builder(val redisClient: RedisClient) {
|
||||||
val redis: RedisAsyncCommands<String, String> = redisClient.connect()?.async() ?: error("Unable to connect to redis")
|
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
|
||||||
val key = "notification:${citizen.id}"
|
private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis")
|
||||||
|
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
citizen: CitizenI,
|
||||||
|
incoming: Flow<Notification>,
|
||||||
|
onRecieve: suspend (Notification) -> Unit,
|
||||||
|
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve)
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
|
||||||
|
/* Convert channel of string from websocket, to a flow of Notification object */
|
||||||
|
val incomingFlow: Flow<Notification> = ws.incoming.consumeAsFlow()
|
||||||
|
.mapNotNull<Frame, Text> { it as? Frame.Text }
|
||||||
|
.map { it.readText() }
|
||||||
|
.map { Notification.fromString(it) }
|
||||||
|
|
||||||
|
return build(ws.call.citizen, incomingFlow) {
|
||||||
|
ws.outgoing.send(Text(it.toString()))
|
||||||
|
}.apply {
|
||||||
|
ws.outgoing.invokeOnClose { close() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val key = "notification:${citizen.id}"
|
||||||
private var score: Double = 0.0
|
private var score: Double = 0.0
|
||||||
|
private val listener = object : RedisPubSubAdapter<String, String>() {
|
||||||
|
/* On new key publish */
|
||||||
|
override fun message(pattern: String?, channel: String?, message: String?) {
|
||||||
|
runBlocking {
|
||||||
|
getNotifications().collect {
|
||||||
|
onRecieve(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
/* Mark as read all incoming notifications */
|
/* Mark as read all incoming notifications */
|
||||||
@@ -44,21 +90,16 @@ class NotificationsPush (
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Lisen redis event, and sent the new notification into websocket */
|
/* Lisen redis event, and sent the new notification into websocket */
|
||||||
redisClient.connectPubSub()?.run {
|
redisConnectionPubSub.run {
|
||||||
addListener(object : RedisPubSubAdapter<String, String>() {
|
addListener(listener)
|
||||||
/* On new key publish */
|
|
||||||
override fun message(pattern: String?, channel: String?, message: String?) {
|
|
||||||
runBlocking {
|
|
||||||
getNotifications().collect {
|
|
||||||
onRecieve(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/* Register to the events */
|
/* Register to the events */
|
||||||
async()?.psubscribe("__key*__:$key") ?: error("Unable to connect to redis")
|
async()?.psubscribe("__key*__:$key") ?: error("Unable to connect to redis")
|
||||||
} ?: error("PubSub Fail")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
redisConnectionPubSub.removeListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Return flow with all new notifications */
|
/* Return flow with all new notifications */
|
||||||
@@ -92,4 +133,4 @@ class NotificationsPush (
|
|||||||
.error("Unable to deserialize notification")
|
.error("Unable to deserialize notification")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
package fr.dcproject.routes
|
package fr.dcproject.routes
|
||||||
|
|
||||||
import fr.dcproject.component.auth.citizen
|
|
||||||
import fr.dcproject.notification.Notification
|
|
||||||
import io.ktor.http.cio.websocket.Frame
|
|
||||||
import io.ktor.http.cio.websocket.Frame.Text
|
|
||||||
import io.ktor.http.cio.websocket.readText
|
|
||||||
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
|
||||||
import io.lettuce.core.RedisClient
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.consumeAsFlow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import notification.NotificationsPush
|
import notification.NotificationsPush
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,18 +13,8 @@ import notification.NotificationsPush
|
|||||||
*/
|
*/
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
fun Route.notificationArticle(redisClient: RedisClient) {
|
fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) {
|
||||||
webSocket("/notifications") {
|
webSocket("/notifications") {
|
||||||
/* Convert channel of string from websocket, to a flow of Notification object */
|
pushBuilder.build(this)
|
||||||
val incomingFlow: Flow<Notification> = incoming.consumeAsFlow()
|
|
||||||
.mapNotNull<Frame, Text> { it as? Frame.Text }
|
|
||||||
.map { it.readText() }
|
|
||||||
.map { Notification.fromString(it) }
|
|
||||||
|
|
||||||
/* Read user notifications in redis then sent it to the websocket */
|
|
||||||
NotificationsPush(redisClient, call.citizen, incomingFlow) {
|
|
||||||
outgoing.send(Text(it.toString()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import com.rabbitmq.client.Channel
|
||||||
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import fr.dcproject.application.Configuration
|
import fr.dcproject.application.Configuration
|
||||||
import fr.dcproject.application.Env.CUCUMBER
|
import fr.dcproject.application.Env.CUCUMBER
|
||||||
import fr.dcproject.application.module
|
import fr.dcproject.application.module
|
||||||
@@ -10,6 +12,8 @@ import io.cucumber.java8.Scenario
|
|||||||
import io.cucumber.junit.Cucumber
|
import io.cucumber.junit.Cucumber
|
||||||
import io.cucumber.junit.CucumberOptions
|
import io.cucumber.junit.CucumberOptions
|
||||||
import io.ktor.server.testing.withTestApplication
|
import io.ktor.server.testing.withTestApplication
|
||||||
|
import io.lettuce.core.RedisClient
|
||||||
|
import io.lettuce.core.api.sync.RedisCommands
|
||||||
import kotlinx.coroutines.InternalCoroutinesApi
|
import kotlinx.coroutines.InternalCoroutinesApi
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koin.test.KoinTest
|
import org.koin.test.KoinTest
|
||||||
@@ -24,6 +28,12 @@ var unitialized: Boolean = false
|
|||||||
@CucumberOptions(plugin = ["pretty"], strict = true)
|
@CucumberOptions(plugin = ["pretty"], strict = true)
|
||||||
class CucumberTest : En, KoinTest {
|
class CucumberTest : En, KoinTest {
|
||||||
private val logger: Logger? by LoggerDelegate()
|
private val logger: Logger? by LoggerDelegate()
|
||||||
|
val config = Configuration("application-test.conf")
|
||||||
|
val redis: RedisCommands<String, String> = RedisClient.create(config.redis).connect().sync()
|
||||||
|
val rabbit: Channel = ConnectionFactory()
|
||||||
|
.apply { setUri(config.rabbitmq) }
|
||||||
|
.newConnection()
|
||||||
|
.createChannel()
|
||||||
|
|
||||||
@InternalCoroutinesApi
|
@InternalCoroutinesApi
|
||||||
val ktorContext = KtorServerContext {
|
val ktorContext = KtorServerContext {
|
||||||
@@ -47,6 +57,14 @@ class CucumberTest : En, KoinTest {
|
|||||||
After { _: Scenario ->
|
After { _: Scenario ->
|
||||||
//language=PostgreSQL
|
//language=PostgreSQL
|
||||||
get<Connection>().sendQuery("rollback;", listOf())
|
get<Connection>().sendQuery("rollback;", listOf())
|
||||||
|
|
||||||
|
redis.flushall()
|
||||||
|
/* Purge rabbit notification queues */
|
||||||
|
rabbit.run {
|
||||||
|
queuePurge("push")
|
||||||
|
queuePurge("email")
|
||||||
|
}
|
||||||
|
|
||||||
ktorContext.stop()
|
ktorContext.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +93,7 @@ class CucumberTest : En, KoinTest {
|
|||||||
private fun getFixturesRequester(): Requester {
|
private fun getFixturesRequester(): Requester {
|
||||||
return Requester.RequesterFactory(
|
return Requester.RequesterFactory(
|
||||||
connection = get(),
|
connection = get(),
|
||||||
queriesDirectory = Configuration.Sql.fixtureFiles
|
queriesDirectory = config.sql.fixtureFiles
|
||||||
).createRequester()
|
).createRequester()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import io.ktor.server.testing.withTestApplication
|
|||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
import kotlinx.coroutines.InternalCoroutinesApi
|
import kotlinx.coroutines.InternalCoroutinesApi
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
import org.junit.jupiter.api.Tags
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.koin.test.AutoCloseKoinTest
|
import org.koin.test.AutoCloseKoinTest
|
||||||
@@ -20,10 +21,11 @@ import org.koin.test.get
|
|||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
@KtorExperimentalAPI
|
@KtorExperimentalAPI
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
@Tags(Tag("functional"))
|
||||||
class MailerTest : KoinTest, AutoCloseKoinTest() {
|
class MailerTest : KoinTest, AutoCloseKoinTest() {
|
||||||
@InternalCoroutinesApi
|
@InternalCoroutinesApi
|
||||||
@Test
|
@Test
|
||||||
@Tag("online, functional")
|
@Tags(Tag("online"))
|
||||||
fun `can be send an email`() {
|
fun `can be send an email`() {
|
||||||
withTestApplication({ module(TEST) }) {
|
withTestApplication({ module(TEST) }) {
|
||||||
get<Mailer>().sendEmail {
|
get<Mailer>().sendEmail {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import fr.dcproject.component.article.ArticleRef
|
|||||||
import fr.dcproject.component.citizen.CitizenRef
|
import fr.dcproject.component.citizen.CitizenRef
|
||||||
import fr.dcproject.component.follow.FollowArticleRepository
|
import fr.dcproject.component.follow.FollowArticleRepository
|
||||||
import fr.dcproject.component.follow.FollowSimple
|
import fr.dcproject.component.follow.FollowSimple
|
||||||
|
import fr.dcproject.messages.NotificationEmailSender
|
||||||
import fr.dcproject.notification.ArticleUpdateNotification
|
import fr.dcproject.notification.ArticleUpdateNotification
|
||||||
import fr.dcproject.notification.NotificationConsumer
|
import fr.dcproject.notification.NotificationConsumer
|
||||||
import fr.dcproject.notification.publisher.Publisher
|
import fr.dcproject.notification.publisher.Publisher
|
||||||
import fr.dcproject.messages.NotificationEmailSender
|
|
||||||
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
|
||||||
@@ -22,31 +22,53 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|||||||
import kotlinx.coroutines.InternalCoroutinesApi
|
import kotlinx.coroutines.InternalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
|
||||||
|
@Tags(Tag("functional"))
|
||||||
class NotificationConsumerTest {
|
class NotificationConsumerTest {
|
||||||
|
companion object {
|
||||||
|
@BeforeAll
|
||||||
|
@JvmStatic
|
||||||
|
fun before() {
|
||||||
|
val config: Configuration = Configuration("application-test.conf")
|
||||||
|
RedisClient.create(config.redis).connect().sync().flushall()
|
||||||
|
|
||||||
|
/* Purge rabbit notification queues */
|
||||||
|
ConnectionFactory()
|
||||||
|
.apply { setUri(config.rabbitmq) }
|
||||||
|
.run {
|
||||||
|
newConnection().createChannel().apply {
|
||||||
|
queuePurge("push")
|
||||||
|
queuePurge("email")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@InternalCoroutinesApi
|
@InternalCoroutinesApi
|
||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
@KtorExperimentalAPI
|
@KtorExperimentalAPI
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@Test
|
@Test
|
||||||
@Tag("functional")
|
|
||||||
fun `can be send notification`() = runBlocking {
|
fun `can be send notification`() = runBlocking {
|
||||||
|
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>() {
|
||||||
every { sendEmail(any()) } returns Unit
|
every { sendEmail(any()) } returns Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Init Spy on redis client */
|
/* Init Spy on redis client */
|
||||||
val redisClient = spyk<RedisClient>(RedisClient.create(Configuration.redis))
|
val redisClient = spyk<RedisClient>(RedisClient.create(config.redis))
|
||||||
val asyncCommand = spyk(redisClient.connect().async())
|
val asyncCommand = spyk(redisClient.connect().async())
|
||||||
every { redisClient.connect().async() } returns asyncCommand
|
every { redisClient.connect().async() } returns asyncCommand
|
||||||
|
|
||||||
val rabbitFactory: ConnectionFactory = spyk {
|
val rabbitFactory: ConnectionFactory = spyk {
|
||||||
ConnectionFactory().apply { setUri(Configuration.rabbitmq) }
|
ConnectionFactory().apply { setUri(config.rabbitmq) }
|
||||||
}
|
}
|
||||||
val followArticleRepo = mockk<FollowArticleRepository> {
|
val followArticleRepo = mockk<FollowArticleRepository> {
|
||||||
every { findFollowsByTarget(any()) } returns flow {
|
every { findFollowsByTarget(any()) } returns flow {
|
||||||
@@ -57,21 +79,15 @@ class NotificationConsumerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Purge rabbit notification queues */
|
|
||||||
rabbitFactory.newConnection().createChannel().apply {
|
|
||||||
queuePurge("push")
|
|
||||||
queuePurge("email")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Config consumer */
|
/* Config consumer */
|
||||||
NotificationConsumer(
|
val consumer = NotificationConsumer(
|
||||||
rabbitFactory = rabbitFactory,
|
rabbitFactory = rabbitFactory,
|
||||||
redisClient = redisClient,
|
redisClient = redisClient,
|
||||||
followArticleRepo = followArticleRepo,
|
followArticleRepo = followArticleRepo,
|
||||||
followConstitutionRepo = mockk(),
|
followConstitutionRepo = mockk(),
|
||||||
notificationEmailSender = emailSender,
|
notificationEmailSender = emailSender,
|
||||||
exchangeName = "notification_test",
|
exchangeName = "notification_test",
|
||||||
).config()
|
).apply { start() }
|
||||||
verify { rabbitFactory.newConnection() }
|
verify { rabbitFactory.newConnection() }
|
||||||
|
|
||||||
/* Push message */
|
/* Push message */
|
||||||
@@ -93,5 +109,7 @@ class NotificationConsumerTest {
|
|||||||
verify(timeout = 1000) { followArticleRepo.findFollowsByTarget(any()) }
|
verify(timeout = 1000) { followArticleRepo.findFollowsByTarget(any()) }
|
||||||
verify(timeout = 1000) { emailSender.sendEmail(any()) }
|
verify(timeout = 1000) { emailSender.sendEmail(any()) }
|
||||||
verify(timeout = 1000) { asyncCommand.zadd(any<String>(), any<Double>(), any<String>()) }
|
verify(timeout = 1000) { asyncCommand.zadd(any<String>(), any<Double>(), any<String>()) }
|
||||||
|
|
||||||
|
// consumer.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,54 @@
|
|||||||
package functional
|
package functional
|
||||||
|
|
||||||
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import fr.dcproject.application.Configuration
|
import fr.dcproject.application.Configuration
|
||||||
import fr.dcproject.component.article.ArticleForView
|
import fr.dcproject.component.article.ArticleForView
|
||||||
import fr.dcproject.component.citizen.CitizenRef
|
import fr.dcproject.component.citizen.CitizenRef
|
||||||
import fr.dcproject.notification.ArticleUpdateNotification
|
import fr.dcproject.notification.ArticleUpdateNotification
|
||||||
import fr.dcproject.notification.Notification
|
import fr.dcproject.notification.Notification
|
||||||
import io.lettuce.core.Limit
|
|
||||||
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
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import notification.NotificationsPush
|
import notification.NotificationsPush
|
||||||
import org.amshove.kluent.`should be equal to`
|
import org.amshove.kluent.`should be equal to`
|
||||||
import org.junit.jupiter.api.Assertions.*
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
import org.junit.jupiter.api.Tags
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.koin.test.AutoCloseKoinTest
|
import kotlin.test.assertEquals
|
||||||
import org.koin.test.KoinTest
|
|
||||||
|
|
||||||
|
@Tags(Tag("functional"))
|
||||||
internal class NotificationsPushTest {
|
internal class NotificationsPushTest {
|
||||||
|
companion object {
|
||||||
|
@BeforeAll
|
||||||
|
@JvmStatic
|
||||||
|
fun before() {
|
||||||
|
val config: Configuration = Configuration("application-test.conf")
|
||||||
|
RedisClient.create(config.redis).connect().sync().flushall()
|
||||||
|
|
||||||
|
/* Purge rabbit notification queues */
|
||||||
|
ConnectionFactory()
|
||||||
|
.apply { setUri(config.rabbitmq) }
|
||||||
|
.newConnection().createChannel().apply {
|
||||||
|
queuePurge("push")
|
||||||
|
queuePurge("email")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Tag("functional")
|
|
||||||
fun `Notification from redis is well catch and return`() = runBlocking {
|
fun `Notification from redis is well catch and return`() = runBlocking {
|
||||||
|
val config: Configuration = Configuration("application-test.conf")
|
||||||
/* Redis client for test */
|
/* Redis client for test */
|
||||||
val redisClientTest = RedisClient.create(Configuration.redis)
|
val redisClientTest = RedisClient.create(config.redis)
|
||||||
|
|
||||||
/* Init Spy on redis client */
|
/* Init Spy on redis client */
|
||||||
val redisClient = spyk<RedisClient>(RedisClient.create(Configuration.redis))
|
val redisClient = spyk<RedisClient>(RedisClient.create(config.redis))
|
||||||
val asyncCommand = spyk(redisClient.connect().async())
|
val asyncCommand = spyk(redisClient.connect().async())
|
||||||
every { redisClient.connect().async() } returns asyncCommand
|
every { redisClient.connect().async() } returns asyncCommand
|
||||||
|
|
||||||
@@ -44,36 +63,41 @@ internal class NotificationsPushTest {
|
|||||||
)
|
)
|
||||||
/* 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 = ArticleUpdateNotification(article)
|
||||||
|
runBlocking {
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
val notifAfterSubscribe = ArticleUpdateNotification(article)
|
val notifAfterSubscribe = ArticleUpdateNotification(article)
|
||||||
|
|
||||||
/* init event for emulate incomint message from websocket */
|
/* init event for emulate incomint message from websocket */
|
||||||
val event = MutableSharedFlow<Notification>()
|
val event = MutableSharedFlow<Notification>()
|
||||||
val incomingFlow = event.asSharedFlow()
|
val incomingFlow = event.asSharedFlow()
|
||||||
|
|
||||||
spyk(object { var counter = 0}).run { /* Counter for count the callback of notification */
|
spyk(object { var counter = 0 }).run { /* Counter for count the callback of notification */
|
||||||
/* Sent notification */
|
/* Sent notification */
|
||||||
redisClientTest.connect().sync().run {
|
redisClientTest.connect().run {
|
||||||
zadd(
|
sync().zadd(
|
||||||
"notification:${citizen.id}",
|
"notification:${citizen.id}",
|
||||||
notifBeforeSubscribe.id,
|
notifBeforeSubscribe.id,
|
||||||
notifBeforeSubscribe.toString()
|
notifBeforeSubscribe.toString()
|
||||||
)
|
)
|
||||||
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Init NotificationPush system, and set assertion in callback */
|
/* Init NotificationPush system, and set assertion in callback */
|
||||||
NotificationsPush(redisClient, citizen, incomingFlow) {
|
val notificationPush = NotificationsPush.Builder(redisClient).build(citizen, incomingFlow) {
|
||||||
counter++
|
counter++
|
||||||
if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id
|
if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id
|
||||||
else it.id `should be equal to` notifAfterSubscribe.id
|
else it.id `should be equal to` notifAfterSubscribe.id
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sent the notification */
|
/* Sent the notification */
|
||||||
redisClientTest.connect().sync().run {
|
redisClientTest.connect().run {
|
||||||
zadd(
|
sync().zadd(
|
||||||
"notification:${citizen.id}",
|
"notification:${citizen.id}",
|
||||||
notifAfterSubscribe.id,
|
notifAfterSubscribe.id,
|
||||||
notifAfterSubscribe.toString()
|
notifAfterSubscribe.toString()
|
||||||
)
|
)
|
||||||
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verify if the callback is called 2 times */
|
/* Verify if the callback is called 2 times */
|
||||||
@@ -83,7 +107,8 @@ internal class NotificationsPushTest {
|
|||||||
/* Emit an event to delete notification */
|
/* Emit an event to delete notification */
|
||||||
event.emit(notifAfterSubscribe)
|
event.emit(notifAfterSubscribe)
|
||||||
/* Verify the "mark as read" is called */
|
/* Verify the "mark as read" is called */
|
||||||
verify(timeout = 300) { asyncCommand.zremrangebyscore(any(), any()) }
|
verify(timeout = 500) { asyncCommand.zremrangebyscore(any(), any()) }
|
||||||
|
notificationPush.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package functional
|
package functional
|
||||||
|
|
||||||
import fr.dcproject.utils.readResource
|
import fr.dcproject.utils.readResource
|
||||||
|
import org.junit.jupiter.api.Tag
|
||||||
|
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 kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
@Tags(Tag("functional"))
|
||||||
class ResourcesKtTest {
|
class ResourcesKtTest {
|
||||||
@Test
|
@Test
|
||||||
fun readResource() {
|
fun readResource() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import io.ktor.server.testing.withTestApplication
|
|||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
import org.amshove.kluent.`should be equal to`
|
import org.amshove.kluent.`should be equal to`
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
||||||
@@ -19,7 +20,7 @@ import java.util.UUID
|
|||||||
@KtorExperimentalLocationsAPI
|
@KtorExperimentalLocationsAPI
|
||||||
@KtorExperimentalAPI
|
@KtorExperimentalAPI
|
||||||
@TestInstance(PER_CLASS)
|
@TestInstance(PER_CLASS)
|
||||||
@Tag("functional")
|
@Tags(Tag("functional"))
|
||||||
class ViewTest {
|
class ViewTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test View Article`() {
|
fun `test View Article`() {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import io.mockk.mockk
|
|||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
@@ -23,7 +24,7 @@ import fr.dcproject.component.article.ArticleRepository as ArticleRepo
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tag("security")
|
@Tags(Tag("security"), Tag("unit"))
|
||||||
internal class ArticleAccessControlTest {
|
internal class ArticleAccessControlTest {
|
||||||
private val tesla = CitizenCart(
|
private val tesla = CitizenCart(
|
||||||
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
|
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
|
|||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
@@ -17,7 +18,7 @@ import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tag("security")
|
@Tags(Tag("security"), Tag("unit"))
|
||||||
internal class CitizenAccessControlTest {
|
internal class CitizenAccessControlTest {
|
||||||
private val tesla = CitizenBasic(
|
private val tesla = CitizenBasic(
|
||||||
user = User(
|
user = User(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
|
|||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
@@ -23,7 +24,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tag("security")
|
@Tags(Tag("security"), Tag("unit"))
|
||||||
internal class CommentAccessControlTest {
|
internal class CommentAccessControlTest {
|
||||||
private val tesla = Citizen(
|
private val tesla = Citizen(
|
||||||
user = User(
|
user = User(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
|
|||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
@@ -22,7 +23,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tag("security")
|
@Tags(Tag("security"), Tag("unit"))
|
||||||
internal class FollowAccessControlTest {
|
internal class FollowAccessControlTest {
|
||||||
private val tesla = CitizenBasic(
|
private val tesla = CitizenBasic(
|
||||||
user = User(
|
user = User(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
|
|||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
@@ -22,7 +23,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tag("security")
|
@Tags(Tag("security"), Tag("unit"))
|
||||||
internal class OpinionAccessControlTest {
|
internal class OpinionAccessControlTest {
|
||||||
private val tesla = CitizenBasic(
|
private val tesla = CitizenBasic(
|
||||||
user = User(
|
user = User(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
|
|||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
@@ -20,7 +21,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tag("security")
|
@Tags(Tag("security"), Tag("unit"))
|
||||||
internal class OpinionChoiceAccessControlTest {
|
internal class OpinionChoiceAccessControlTest {
|
||||||
private val tesla = CitizenBasic(
|
private val tesla = CitizenBasic(
|
||||||
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
|
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
|
|||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
@@ -23,7 +24,7 @@ import fr.dcproject.component.vote.entity.Vote as VoteEntity
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tag("security")
|
@Tags(Tag("security"), Tag("unit"))
|
||||||
internal class VoteAccessControlTest {
|
internal class VoteAccessControlTest {
|
||||||
private val tesla = Citizen(
|
private val tesla = Citizen(
|
||||||
id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"),
|
id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
|
|||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.junit.jupiter.api.Tag
|
import org.junit.jupiter.api.Tag
|
||||||
|
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.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
@@ -21,7 +22,7 @@ import fr.dcproject.component.workgroup.Workgroup as WorkgroupEntity
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Execution(CONCURRENT)
|
@Execution(CONCURRENT)
|
||||||
@Tag("security")
|
@Tags(Tag("security"), Tag("unit"))
|
||||||
internal class WorkgroupAccessControlTest {
|
internal class WorkgroupAccessControlTest {
|
||||||
private val tesla = CitizenBasic(
|
private val tesla = CitizenBasic(
|
||||||
user = User(
|
user = User(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ ktor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app {
|
app {
|
||||||
envName = prod
|
envName = test
|
||||||
domain = dc-project.fr
|
domain = dc-project.fr
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user