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 }