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
}