From 89c15eb1cf50fd5fdeba0d2c45de63e622f2d267 Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Thu, 4 Feb 2021 02:36:02 +0100 Subject: [PATCH] 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 --- .idea/runConfigurations/Unit_Tests.xml | 2 +- .../Unit_Tests__offline_.xml | 23 ------ src/main/kotlin/application/Application.kt | 8 +- src/main/kotlin/application/Configuration.kt | 40 +++++++--- src/main/kotlin/application/KoinModule.kt | 55 +++++++++---- src/main/kotlin/component/auth/KoinModule.kt | 5 +- src/main/kotlin/component/views/KoinModule.kt | 17 ++-- src/main/kotlin/notification/Notification.kt | 14 +++- .../notification/NotificationConsumer.kt | 16 ++-- .../kotlin/notification/NotificationsPush.kt | 79 ++++++++++++++----- src/main/kotlin/routes/Notification.kt | 24 +----- src/test/kotlin/CucumberTest.kt | 20 ++++- src/test/kotlin/functional/MailerTest.kt | 4 +- .../functional/NotificationConsumerTest.kt | 44 ++++++++--- .../functional/NotificationsPushTest.kt | 55 +++++++++---- src/test/kotlin/functional/ResourcesKtTest.kt | 3 + src/test/kotlin/functional/ViewTest.kt | 3 +- .../unit/security/ArticleAccessControlTest.kt | 3 +- .../unit/security/CitizenAccessControlTest.kt | 3 +- .../unit/security/CommentAccessControlTest.kt | 3 +- .../unit/security/FollowAccessControlTest.kt | 3 +- .../unit/security/OpinionAccessControlTest.kt | 3 +- .../OpinionChoiceAccessControlTest.kt | 3 +- .../unit/security/VoteAccessControlTest.kt | 3 +- .../security/WorkgroupAccessControlTest.kt | 3 +- ...application.conf => application-test.conf} | 2 +- 26 files changed, 289 insertions(+), 149 deletions(-) delete mode 100644 .idea/runConfigurations/Unit_Tests__offline_.xml rename src/test/resources/{application.conf => application-test.conf} (97%) diff --git a/.idea/runConfigurations/Unit_Tests.xml b/.idea/runConfigurations/Unit_Tests.xml index a8e76aa..2b42cda 100644 --- a/.idea/runConfigurations/Unit_Tests.xml +++ b/.idea/runConfigurations/Unit_Tests.xml @@ -15,7 +15,7 @@ - + diff --git a/.idea/runConfigurations/Unit_Tests__offline_.xml b/.idea/runConfigurations/Unit_Tests__offline_.xml deleted file mode 100644 index 159a284..0000000 --- a/.idea/runConfigurations/Unit_Tests__offline_.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/kotlin/application/Application.kt b/src/main/kotlin/application/Application.kt index 3831aaf..c89008b 100644 --- a/src/main/kotlin/application/Application.kt +++ b/src/main/kotlin/application/Application.kt @@ -39,6 +39,7 @@ import fr.dcproject.routes.notificationArticle import fr.dcproject.security.AccessDeniedException import fr.postgresjson.migration.Migrations import io.ktor.application.Application +import io.ktor.application.ApplicationStopped import io.ktor.application.call import io.ktor.application.install import io.ktor.auth.Authentication @@ -122,7 +123,12 @@ fun Application.module(env: Env = PROD) { masking = false } - NotificationConsumer(get(), get(), get(), get(), get(), Configuration.exchangeNotificationName).config() + get().run { + start() + environment.monitor.subscribe(ApplicationStopped) { + close() + } + } install(Authentication, jwtInstallation(get())) diff --git a/src/main/kotlin/application/Configuration.kt b/src/main/kotlin/application/Configuration.kt index 0ee858c..922e199 100644 --- a/src/main/kotlin/application/Configuration.kt +++ b/src/main/kotlin/application/Configuration.kt @@ -1,23 +1,39 @@ package fr.dcproject.application +import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import java.net.URI -object Configuration { - private var config = ConfigFactory.load() +class Configuration(val config: Config) { + constructor(resourceBasename: String? = null) : this(if (resourceBasename == null) ConfigFactory.load() else ConfigFactory.load(resourceBasename)) - object Sql { - val migrationFiles: URI = this::class.java.getResource("/sql/migrations")?.toURI() ?: error("No migrations found") - val functionFiles: URI = this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found") - val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures")?.toURI() ?: error("No sql fixture found") + interface Sql { + val migrationFiles: URI + val functionFiles: URI + val fixtureFiles: URI } - object Database { - val host: String = config.getString("db.host") - val port: Int = config.getInt("db.port") - var database: String = config.getString("db.database") - var username: String = config.getString("db.username") - var password: String = config.getString("db.password") + val sql + get() = object : Sql { + override val migrationFiles: URI = this::class.java.getResource("/sql/migrations")?.toURI() ?: error("No migrations found") + override val functionFiles: URI = this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found") + override val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures")?.toURI() ?: error("No sql fixture found") + } + + 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 domain: String = config.getString("app.domain") diff --git a/src/main/kotlin/application/KoinModule.kt b/src/main/kotlin/application/KoinModule.kt index 32d8a3b..bdf9afa 100644 --- a/src/main/kotlin/application/KoinModule.kt +++ b/src/main/kotlin/application/KoinModule.kt @@ -8,9 +8,10 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.datatype.joda.JodaModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.rabbitmq.client.ConnectionFactory -import fr.dcproject.notification.publisher.Publisher import fr.dcproject.messages.Mailer 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.Requester import fr.postgresjson.migration.Migrations @@ -18,36 +19,52 @@ import io.ktor.client.HttpClient import io.ktor.client.features.websocket.WebSockets import io.ktor.util.KtorExperimentalAPI import io.lettuce.core.RedisClient -import io.lettuce.core.api.async.RedisAsyncCommands +import notification.NotificationsPush import org.koin.core.qualifier.named import org.koin.dsl.module +import org.koin.ktor.ext.get @KtorExperimentalAPI val KoinModule = module { + single { Configuration() } + // SQL connection single { + val config: Configuration = get() Connection( - host = Configuration.Database.host, - port = Configuration.Database.port, - database = Configuration.Database.database, - username = Configuration.Database.username, - password = Configuration.Database.password + host = config.database.host, + port = config.database.port, + database = config.database.database, + username = config.database.username, + password = config.database.password ) } // 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 single { - RedisClient.create(Configuration.redis).apply { + val config: Configuration = get() + RedisClient.create(config.redis).apply { 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 single { - ConnectionFactory().apply { setUri(Configuration.rabbitmq) } + val config: Configuration = get() + ConnectionFactory().apply { setUri(config.rabbitmq) } } // JsonSerializer @@ -71,16 +88,26 @@ val KoinModule = module { // SQL Requester (postgresJson) single { + val config: Configuration = get() Requester.RequesterFactory( connection = get(), - functionsDirectory = Configuration.Sql.functionFiles + functionsDirectory = config.sql.functionFiles ).createRequester() } // 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(), Configuration.domain, get(), get()) } + single { + val config: Configuration = get() + NotificationEmailSender(get(), config.domain, get(), get()) + } } diff --git a/src/main/kotlin/component/auth/KoinModule.kt b/src/main/kotlin/component/auth/KoinModule.kt index cdf3d91..24d3fa0 100644 --- a/src/main/kotlin/component/auth/KoinModule.kt +++ b/src/main/kotlin/component/auth/KoinModule.kt @@ -7,5 +7,8 @@ import org.koin.dsl.module val authKoinModule = module { single { UserRepository(get()) } // Used to send a connexion link by email - single { PasswordlessAuth(get(), Configuration.domain, get()) } + single { + val config: Configuration = get() + PasswordlessAuth(get(), config.domain, get()) + } } diff --git a/src/main/kotlin/component/views/KoinModule.kt b/src/main/kotlin/component/views/KoinModule.kt index 10916a3..d32b352 100644 --- a/src/main/kotlin/component/views/KoinModule.kt +++ b/src/main/kotlin/component/views/KoinModule.kt @@ -8,12 +8,15 @@ import org.elasticsearch.client.RestClient import org.koin.dsl.module val viewKoinModule = module { - // Elasticsearch Client - val esClient = RestClient.builder( - HttpHost.create(Configuration.elasticsearch) - ).build().apply { - createEsIndexForViews() - } - single { ArticleViewManager(esClient) } + single { + val config: Configuration = get() + // Elasticsearch Client + val esClient = RestClient.builder( + HttpHost.create(config.elasticsearch) + ).build().apply { + createEsIndexForViews() + } + ArticleViewManager(esClient) + } } diff --git a/src/main/kotlin/notification/Notification.kt b/src/main/kotlin/notification/Notification.kt index c074fbe..ab6fd02 100644 --- a/src/main/kotlin/notification/Notification.kt +++ b/src/main/kotlin/notification/Notification.kt @@ -10,16 +10,16 @@ import com.fasterxml.jackson.module.kotlin.readValue import fr.dcproject.component.article.ArticleForView import fr.postgresjson.entity.UuidEntity import org.joda.time.DateTime -import kotlin.random.Random +import java.util.concurrent.atomic.AtomicInteger open class Notification( val type: String, val createdAt: DateTime = DateTime.now() ) { - val id: Double = randId(createdAt.millis) + val id: Double = nextId() - private fun randId(time: Long): Double { - return (time.toString() + Random.nextInt(1000, 9999).toString()).toDouble() + private fun nextId(): Double { + return (createdAt.millis.toString() + nextInt().toString()).toDouble() } override fun toString(): String = mapper.writeValueAsString(this) ?: error("Unable to serialize notification") @@ -27,6 +27,12 @@ open class Notification( fun toByteArray() = toString().toByteArray() companion object { + private val counter: AtomicInteger = AtomicInteger(1000) + fun nextInt(): Int { + counter.compareAndSet(9999, 1000) + return counter.incrementAndGet() + } + val mapper = jacksonObjectMapper().apply { registerModule(SimpleModule()) propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE diff --git a/src/main/kotlin/notification/NotificationConsumer.kt b/src/main/kotlin/notification/NotificationConsumer.kt index 8541106..d41c91b 100644 --- a/src/main/kotlin/notification/NotificationConsumer.kt +++ b/src/main/kotlin/notification/NotificationConsumer.kt @@ -29,10 +29,18 @@ class NotificationConsumer( private val notificationEmailSender: NotificationEmailSender, private val exchangeName: String, ) { - val redis: RedisAsyncCommands = redisClient.connect()?.async() ?: error("Unable to connect to redis") + private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis") + private val redis: RedisAsyncCommands = redisConnection.async() ?: error("Unable to connect to redis") + private val rabbitConnection = rabbitFactory.newConnection() + private val rabbitChannel = rabbitConnection.createChannel() private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName) - fun config() { + fun close() { + rabbitChannel.close() + rabbitConnection.close() + } + + fun start() { /* Config Rabbit */ rabbitFactory.newConnection().use { connection -> connection.createChannel().use { channel -> @@ -45,8 +53,6 @@ class NotificationConsumer( } /* Define Consumer */ - val rabbitChannel = rabbitFactory.newConnection().createChannel() - val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) { @Throws(IOException::class) override fun handleDelivery( @@ -106,4 +112,4 @@ class NotificationConsumer( val rawMessage: String, val follow: FollowSimple ) -} \ No newline at end of file +} diff --git a/src/main/kotlin/notification/NotificationsPush.kt b/src/main/kotlin/notification/NotificationsPush.kt index be0a270..a1b5b91 100644 --- a/src/main/kotlin/notification/NotificationsPush.kt +++ b/src/main/kotlin/notification/NotificationsPush.kt @@ -1,34 +1,80 @@ package notification import com.fasterxml.jackson.core.JsonProcessingException +import fr.dcproject.component.auth.citizen import fr.dcproject.component.citizen.CitizenI 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.websocket.DefaultWebSocketServerSession import io.lettuce.core.Limit import io.lettuce.core.Range import io.lettuce.core.Range.Boundary import io.lettuce.core.RedisClient import io.lettuce.core.api.async.RedisAsyncCommands import io.lettuce.core.pubsub.RedisPubSubAdapter +import io.lettuce.core.pubsub.StatefulRedisPubSubConnection +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory -class NotificationsPush ( - val redisClient: RedisClient, +class NotificationsPush private constructor( + private val redis: RedisAsyncCommands, + private val redisConnectionPubSub: StatefulRedisPubSubConnection, citizen: CitizenI, incoming: Flow, onRecieve: suspend (Notification) -> Unit, -) -{ - val redis: RedisAsyncCommands = redisClient.connect()?.async() ?: error("Unable to connect to redis") - val key = "notification:${citizen.id}" +) { + class Builder(val redisClient: RedisClient) { + private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis") + private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis") + private val redis: RedisAsyncCommands = redisConnection.async() ?: error("Unable to connect to redis") + + fun build( + citizen: CitizenI, + incoming: Flow, + 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 = ws.incoming.consumeAsFlow() + .mapNotNull { 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 val listener = object : RedisPubSubAdapter() { + /* On new key publish */ + override fun message(pattern: String?, channel: String?, message: String?) { + runBlocking { + getNotifications().collect { + onRecieve(it) + } + } + } + } init { /* Mark as read all incoming notifications */ @@ -44,21 +90,16 @@ class NotificationsPush ( } /* Lisen redis event, and sent the new notification into websocket */ - redisClient.connectPubSub()?.run { - addListener(object : RedisPubSubAdapter() { - /* On new key publish */ - override fun message(pattern: String?, channel: String?, message: String?) { - runBlocking { - getNotifications().collect { - onRecieve(it) - } - } - } - }) + redisConnectionPubSub.run { + addListener(listener) /* Register to the events */ async()?.psubscribe("__key*__:$key") ?: error("Unable to connect to redis") - } ?: error("PubSub Fail") + } + } + + fun close() { + redisConnectionPubSub.removeListener(listener) } /* Return flow with all new notifications */ @@ -92,4 +133,4 @@ class NotificationsPush ( .error("Unable to deserialize notification") } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/routes/Notification.kt b/src/main/kotlin/routes/Notification.kt index 4df3ba7..106752b 100644 --- a/src/main/kotlin/routes/Notification.kt +++ b/src/main/kotlin/routes/Notification.kt @@ -1,19 +1,9 @@ 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.routing.Route import io.ktor.websocket.webSocket -import io.lettuce.core.RedisClient 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 /** @@ -23,18 +13,8 @@ import notification.NotificationsPush */ @ExperimentalCoroutinesApi @KtorExperimentalLocationsAPI -fun Route.notificationArticle(redisClient: RedisClient) { +fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) { webSocket("/notifications") { - /* Convert channel of string from websocket, to a flow of Notification object */ - val incomingFlow: Flow = incoming.consumeAsFlow() - .mapNotNull { 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())) - } + pushBuilder.build(this) } } - diff --git a/src/test/kotlin/CucumberTest.kt b/src/test/kotlin/CucumberTest.kt index e71bd8b..c11b773 100644 --- a/src/test/kotlin/CucumberTest.kt +++ b/src/test/kotlin/CucumberTest.kt @@ -1,3 +1,5 @@ +import com.rabbitmq.client.Channel +import com.rabbitmq.client.ConnectionFactory import fr.dcproject.application.Configuration import fr.dcproject.application.Env.CUCUMBER import fr.dcproject.application.module @@ -10,6 +12,8 @@ import io.cucumber.java8.Scenario import io.cucumber.junit.Cucumber import io.cucumber.junit.CucumberOptions import io.ktor.server.testing.withTestApplication +import io.lettuce.core.RedisClient +import io.lettuce.core.api.sync.RedisCommands import kotlinx.coroutines.InternalCoroutinesApi import org.junit.runner.RunWith import org.koin.test.KoinTest @@ -24,6 +28,12 @@ var unitialized: Boolean = false @CucumberOptions(plugin = ["pretty"], strict = true) class CucumberTest : En, KoinTest { private val logger: Logger? by LoggerDelegate() + val config = Configuration("application-test.conf") + val redis: RedisCommands = RedisClient.create(config.redis).connect().sync() + val rabbit: Channel = ConnectionFactory() + .apply { setUri(config.rabbitmq) } + .newConnection() + .createChannel() @InternalCoroutinesApi val ktorContext = KtorServerContext { @@ -47,6 +57,14 @@ class CucumberTest : En, KoinTest { After { _: Scenario -> //language=PostgreSQL get().sendQuery("rollback;", listOf()) + + redis.flushall() + /* Purge rabbit notification queues */ + rabbit.run { + queuePurge("push") + queuePurge("email") + } + ktorContext.stop() } } @@ -75,7 +93,7 @@ class CucumberTest : En, KoinTest { private fun getFixturesRequester(): Requester { return Requester.RequesterFactory( connection = get(), - queriesDirectory = Configuration.Sql.fixtureFiles + queriesDirectory = config.sql.fixtureFiles ).createRequester() } } diff --git a/src/test/kotlin/functional/MailerTest.kt b/src/test/kotlin/functional/MailerTest.kt index 247c256..56d9811 100644 --- a/src/test/kotlin/functional/MailerTest.kt +++ b/src/test/kotlin/functional/MailerTest.kt @@ -11,6 +11,7 @@ import io.ktor.server.testing.withTestApplication import io.ktor.util.KtorExperimentalAPI import kotlinx.coroutines.InternalCoroutinesApi import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.koin.test.AutoCloseKoinTest @@ -20,10 +21,11 @@ import org.koin.test.get @KtorExperimentalLocationsAPI @KtorExperimentalAPI @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tags(Tag("functional")) class MailerTest : KoinTest, AutoCloseKoinTest() { @InternalCoroutinesApi @Test - @Tag("online, functional") + @Tags(Tag("online")) fun `can be send an email`() { withTestApplication({ module(TEST) }) { get().sendEmail { diff --git a/src/test/kotlin/functional/NotificationConsumerTest.kt b/src/test/kotlin/functional/NotificationConsumerTest.kt index b02fdb9..633520d 100644 --- a/src/test/kotlin/functional/NotificationConsumerTest.kt +++ b/src/test/kotlin/functional/NotificationConsumerTest.kt @@ -7,10 +7,10 @@ import fr.dcproject.component.article.ArticleRef import fr.dcproject.component.citizen.CitizenRef import fr.dcproject.component.follow.FollowArticleRepository import fr.dcproject.component.follow.FollowSimple +import fr.dcproject.messages.NotificationEmailSender import fr.dcproject.notification.ArticleUpdateNotification import fr.dcproject.notification.NotificationConsumer import fr.dcproject.notification.publisher.Publisher -import fr.dcproject.messages.NotificationEmailSender import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.util.KtorExperimentalAPI import io.lettuce.core.RedisClient @@ -22,31 +22,53 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeAll 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) +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@Tags(Tag("functional")) 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 @KtorExperimentalLocationsAPI @KtorExperimentalAPI @ExperimentalCoroutinesApi @Test - @Tag("functional") fun `can be send notification`() = runBlocking { + val config: Configuration = Configuration("application-test.conf") /* Create mocks and spy's */ val emailSender = mockk() { every { sendEmail(any()) } returns Unit } /* Init Spy on redis client */ - val redisClient = spyk(RedisClient.create(Configuration.redis)) + val redisClient = spyk(RedisClient.create(config.redis)) val asyncCommand = spyk(redisClient.connect().async()) every { redisClient.connect().async() } returns asyncCommand val rabbitFactory: ConnectionFactory = spyk { - ConnectionFactory().apply { setUri(Configuration.rabbitmq) } + ConnectionFactory().apply { setUri(config.rabbitmq) } } val followArticleRepo = mockk { 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 */ - NotificationConsumer( + val consumer = NotificationConsumer( rabbitFactory = rabbitFactory, redisClient = redisClient, followArticleRepo = followArticleRepo, followConstitutionRepo = mockk(), notificationEmailSender = emailSender, exchangeName = "notification_test", - ).config() + ).apply { start() } verify { rabbitFactory.newConnection() } /* Push message */ @@ -93,5 +109,7 @@ class NotificationConsumerTest { verify(timeout = 1000) { followArticleRepo.findFollowsByTarget(any()) } verify(timeout = 1000) { emailSender.sendEmail(any()) } verify(timeout = 1000) { asyncCommand.zadd(any(), any(), any()) } + +// consumer.close() } } diff --git a/src/test/kotlin/functional/NotificationsPushTest.kt b/src/test/kotlin/functional/NotificationsPushTest.kt index 2b7ee6a..afd465b 100644 --- a/src/test/kotlin/functional/NotificationsPushTest.kt +++ b/src/test/kotlin/functional/NotificationsPushTest.kt @@ -1,35 +1,54 @@ package functional +import com.rabbitmq.client.ConnectionFactory import fr.dcproject.application.Configuration import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.citizen.CitizenRef import fr.dcproject.notification.ArticleUpdateNotification import fr.dcproject.notification.Notification -import io.lettuce.core.Limit import io.lettuce.core.RedisClient import io.mockk.every import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.runBlocking import notification.NotificationsPush 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.Tags import org.junit.jupiter.api.Test -import org.koin.test.AutoCloseKoinTest -import org.koin.test.KoinTest +import kotlin.test.assertEquals +@Tags(Tag("functional")) 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 - @Tag("functional") fun `Notification from redis is well catch and return`() = runBlocking { + val config: Configuration = Configuration("application-test.conf") /* Redis client for test */ - val redisClientTest = RedisClient.create(Configuration.redis) + val redisClientTest = RedisClient.create(config.redis) /* Init Spy on redis client */ - val redisClient = spyk(RedisClient.create(Configuration.redis)) + val redisClient = spyk(RedisClient.create(config.redis)) val asyncCommand = spyk(redisClient.connect().async()) every { redisClient.connect().async() } returns asyncCommand @@ -44,36 +63,41 @@ internal class NotificationsPushTest { ) /* Init two notification, one called before subscription, and the other after */ val notifBeforeSubscribe = ArticleUpdateNotification(article) + runBlocking { + delay(100) + } val notifAfterSubscribe = ArticleUpdateNotification(article) /* init event for emulate incomint message from websocket */ val event = MutableSharedFlow() 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 */ - redisClientTest.connect().sync().run { - zadd( + redisClientTest.connect().run { + sync().zadd( "notification:${citizen.id}", notifBeforeSubscribe.id, notifBeforeSubscribe.toString() ) + close() } /* Init NotificationPush system, and set assertion in callback */ - NotificationsPush(redisClient, citizen, incomingFlow) { + val notificationPush = NotificationsPush.Builder(redisClient).build(citizen, incomingFlow) { counter++ if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id else it.id `should be equal to` notifAfterSubscribe.id } /* Sent the notification */ - redisClientTest.connect().sync().run { - zadd( + redisClientTest.connect().run { + sync().zadd( "notification:${citizen.id}", notifAfterSubscribe.id, notifAfterSubscribe.toString() ) + close() } /* Verify if the callback is called 2 times */ @@ -83,7 +107,8 @@ internal class NotificationsPushTest { /* Emit an event to delete notification */ event.emit(notifAfterSubscribe) /* Verify the "mark as read" is called */ - verify(timeout = 300) { asyncCommand.zremrangebyscore(any(), any()) } + verify(timeout = 500) { asyncCommand.zremrangebyscore(any(), any()) } + notificationPush.close() } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/functional/ResourcesKtTest.kt b/src/test/kotlin/functional/ResourcesKtTest.kt index 67598fa..0b75d66 100644 --- a/src/test/kotlin/functional/ResourcesKtTest.kt +++ b/src/test/kotlin/functional/ResourcesKtTest.kt @@ -1,11 +1,14 @@ package functional 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.TestInstance import kotlin.test.assertEquals @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tags(Tag("functional")) class ResourcesKtTest { @Test fun readResource() { diff --git a/src/test/kotlin/functional/ViewTest.kt b/src/test/kotlin/functional/ViewTest.kt index 1bfe351..5292d33 100644 --- a/src/test/kotlin/functional/ViewTest.kt +++ b/src/test/kotlin/functional/ViewTest.kt @@ -10,6 +10,7 @@ import io.ktor.server.testing.withTestApplication import io.ktor.util.KtorExperimentalAPI import org.amshove.kluent.`should be equal to` import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS @@ -19,7 +20,7 @@ import java.util.UUID @KtorExperimentalLocationsAPI @KtorExperimentalAPI @TestInstance(PER_CLASS) -@Tag("functional") +@Tags(Tag("functional")) class ViewTest { @Test fun `test View Article`() { diff --git a/src/test/kotlin/unit/security/ArticleAccessControlTest.kt b/src/test/kotlin/unit/security/ArticleAccessControlTest.kt index 2436f0a..4162be0 100644 --- a/src/test/kotlin/unit/security/ArticleAccessControlTest.kt +++ b/src/test/kotlin/unit/security/ArticleAccessControlTest.kt @@ -14,6 +14,7 @@ import io.mockk.mockk import org.amshove.kluent.`should be` import org.joda.time.DateTime import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.parallel.Execution @@ -23,7 +24,7 @@ import fr.dcproject.component.article.ArticleRepository as ArticleRepo @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(CONCURRENT) -@Tag("security") +@Tags(Tag("security"), Tag("unit")) internal class ArticleAccessControlTest { private val tesla = CitizenCart( id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"), diff --git a/src/test/kotlin/unit/security/CitizenAccessControlTest.kt b/src/test/kotlin/unit/security/CitizenAccessControlTest.kt index 7fe0a07..349afa6 100644 --- a/src/test/kotlin/unit/security/CitizenAccessControlTest.kt +++ b/src/test/kotlin/unit/security/CitizenAccessControlTest.kt @@ -10,6 +10,7 @@ import fr.dcproject.security.AccessDecision.GRANTED import org.amshove.kluent.`should be` import org.joda.time.DateTime import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.parallel.Execution @@ -17,7 +18,7 @@ import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(CONCURRENT) -@Tag("security") +@Tags(Tag("security"), Tag("unit")) internal class CitizenAccessControlTest { private val tesla = CitizenBasic( user = User( diff --git a/src/test/kotlin/unit/security/CommentAccessControlTest.kt b/src/test/kotlin/unit/security/CommentAccessControlTest.kt index 16eb5aa..8314c7f 100644 --- a/src/test/kotlin/unit/security/CommentAccessControlTest.kt +++ b/src/test/kotlin/unit/security/CommentAccessControlTest.kt @@ -15,6 +15,7 @@ import fr.dcproject.security.AccessDecision.GRANTED import org.amshove.kluent.`should be` import org.joda.time.DateTime import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.parallel.Execution @@ -23,7 +24,7 @@ import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(CONCURRENT) -@Tag("security") +@Tags(Tag("security"), Tag("unit")) internal class CommentAccessControlTest { private val tesla = Citizen( user = User( diff --git a/src/test/kotlin/unit/security/FollowAccessControlTest.kt b/src/test/kotlin/unit/security/FollowAccessControlTest.kt index 2ec727d..8c2ab9c 100644 --- a/src/test/kotlin/unit/security/FollowAccessControlTest.kt +++ b/src/test/kotlin/unit/security/FollowAccessControlTest.kt @@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED import org.amshove.kluent.`should be` import org.joda.time.DateTime import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.parallel.Execution @@ -22,7 +23,7 @@ import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(CONCURRENT) -@Tag("security") +@Tags(Tag("security"), Tag("unit")) internal class FollowAccessControlTest { private val tesla = CitizenBasic( user = User( diff --git a/src/test/kotlin/unit/security/OpinionAccessControlTest.kt b/src/test/kotlin/unit/security/OpinionAccessControlTest.kt index 31b8649..c888ef7 100644 --- a/src/test/kotlin/unit/security/OpinionAccessControlTest.kt +++ b/src/test/kotlin/unit/security/OpinionAccessControlTest.kt @@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED import org.amshove.kluent.`should be` import org.joda.time.DateTime import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.parallel.Execution @@ -22,7 +23,7 @@ import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(CONCURRENT) -@Tag("security") +@Tags(Tag("security"), Tag("unit")) internal class OpinionAccessControlTest { private val tesla = CitizenBasic( user = User( diff --git a/src/test/kotlin/unit/security/OpinionChoiceAccessControlTest.kt b/src/test/kotlin/unit/security/OpinionChoiceAccessControlTest.kt index 7f2e42f..23b320e 100644 --- a/src/test/kotlin/unit/security/OpinionChoiceAccessControlTest.kt +++ b/src/test/kotlin/unit/security/OpinionChoiceAccessControlTest.kt @@ -12,6 +12,7 @@ import fr.dcproject.security.AccessDecision.GRANTED import org.amshove.kluent.`should be` import org.joda.time.DateTime import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.parallel.Execution @@ -20,7 +21,7 @@ import java.util.UUID @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(CONCURRENT) -@Tag("security") +@Tags(Tag("security"), Tag("unit")) internal class OpinionChoiceAccessControlTest { private val tesla = CitizenBasic( id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"), diff --git a/src/test/kotlin/unit/security/VoteAccessControlTest.kt b/src/test/kotlin/unit/security/VoteAccessControlTest.kt index 9cc5e65..85a2f98 100644 --- a/src/test/kotlin/unit/security/VoteAccessControlTest.kt +++ b/src/test/kotlin/unit/security/VoteAccessControlTest.kt @@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED import org.amshove.kluent.`should be` import org.joda.time.DateTime import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.parallel.Execution @@ -23,7 +24,7 @@ import fr.dcproject.component.vote.entity.Vote as VoteEntity @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(CONCURRENT) -@Tag("security") +@Tags(Tag("security"), Tag("unit")) internal class VoteAccessControlTest { private val tesla = Citizen( id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"), diff --git a/src/test/kotlin/unit/security/WorkgroupAccessControlTest.kt b/src/test/kotlin/unit/security/WorkgroupAccessControlTest.kt index 1af7325..827fe74 100644 --- a/src/test/kotlin/unit/security/WorkgroupAccessControlTest.kt +++ b/src/test/kotlin/unit/security/WorkgroupAccessControlTest.kt @@ -12,6 +12,7 @@ import fr.dcproject.security.AccessDecision.GRANTED import org.amshove.kluent.`should be` import org.joda.time.DateTime import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.parallel.Execution @@ -21,7 +22,7 @@ import fr.dcproject.component.workgroup.Workgroup as WorkgroupEntity @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(CONCURRENT) -@Tag("security") +@Tags(Tag("security"), Tag("unit")) internal class WorkgroupAccessControlTest { private val tesla = CitizenBasic( user = User( diff --git a/src/test/resources/application.conf b/src/test/resources/application-test.conf similarity index 97% rename from src/test/resources/application.conf rename to src/test/resources/application-test.conf index 8961976..0c5f0fd 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application-test.conf @@ -9,7 +9,7 @@ ktor { } app { - envName = prod + envName = test domain = dc-project.fr }