cleanup and refactoring of notification

close rabbit and redis connexion on application close
Refactoring of Configuration class
fix notification id increment
Add builder for NotificationPush
Add close to notificationPush to remove listener
Clean tags of tests
purge queue before functional tests
This commit is contained in:
2021-02-04 02:36:02 +01:00
parent a05b5edc86
commit 89c15eb1cf
26 changed files with 289 additions and 149 deletions

View File

@@ -15,7 +15,7 @@
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" /> <env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs> </envs>
<dir value="$PROJECT_DIR$" /> <dir value="$PROJECT_DIR$" />
<tag value="!functional" /> <tag value="unit" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>

View File

@@ -1,23 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Unit Tests (offline)" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly />
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="tags" />
<option name="VM_PARAMETERS" value="-ea -Dcucumber.options=&quot;--tags ~@online&quot; -Djdk.attach.allowAttachSelf=true" />
<option name="PARAMETERS" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<envs>
<env name="SEND_GRID_KEY" value="$SEND_GRID_KEY$" />
</envs>
<dir value="$PROJECT_DIR$" />
<tag value="!online" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -39,6 +39,7 @@ import fr.dcproject.routes.notificationArticle
import fr.dcproject.security.AccessDeniedException import fr.dcproject.security.AccessDeniedException
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.ApplicationStopped
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
import io.ktor.auth.Authentication import io.ktor.auth.Authentication
@@ -122,7 +123,12 @@ fun Application.module(env: Env = PROD) {
masking = false masking = false
} }
NotificationConsumer(get(), get(), get(), get(), get(), Configuration.exchangeNotificationName).config() get<NotificationConsumer>().run {
start()
environment.monitor.subscribe(ApplicationStopped) {
close()
}
}
install(Authentication, jwtInstallation(get())) install(Authentication, jwtInstallation(get()))

View File

@@ -1,22 +1,38 @@
package fr.dcproject.application package fr.dcproject.application
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import java.net.URI import java.net.URI
object Configuration { class Configuration(val config: Config) {
private var config = ConfigFactory.load() constructor(resourceBasename: String? = null) : this(if (resourceBasename == null) ConfigFactory.load() else ConfigFactory.load(resourceBasename))
object Sql { interface Sql {
val migrationFiles: URI = this::class.java.getResource("/sql/migrations")?.toURI() ?: error("No migrations found") val migrationFiles: URI
val functionFiles: URI = this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found") val functionFiles: URI
val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures")?.toURI() ?: error("No sql fixture found") val fixtureFiles: URI
} }
object Database { val sql
val host: String = config.getString("db.host") get() = object : Sql {
val port: Int = config.getInt("db.port") override val migrationFiles: URI = this::class.java.getResource("/sql/migrations")?.toURI() ?: error("No migrations found")
var database: String = config.getString("db.database") override val functionFiles: URI = this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found")
var username: String = config.getString("db.username") override val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures")?.toURI() ?: error("No sql fixture found")
var password: String = config.getString("db.password") }
interface Database {
val host: String
val port: Int
var database: String
var username: String
var password: String
}
val database
get() = object : Database {
override val host: String = config.getString("db.host")
override val port: Int = config.getInt("db.port")
override var database: String = config.getString("db.database")
override var username: String = config.getString("db.username")
override var password: String = config.getString("db.password")
} }
val envName: String = config.getString("app.envName") val envName: String = config.getString("app.envName")

View File

@@ -8,9 +8,10 @@ import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.joda.JodaModule import com.fasterxml.jackson.datatype.joda.JodaModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.notification.publisher.Publisher
import fr.dcproject.messages.Mailer import fr.dcproject.messages.Mailer
import fr.dcproject.messages.NotificationEmailSender import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.notification.NotificationConsumer
import fr.dcproject.notification.publisher.Publisher
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
@@ -18,36 +19,52 @@ import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.lettuce.core.api.async.RedisAsyncCommands import notification.NotificationsPush
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.ktor.ext.get
@KtorExperimentalAPI @KtorExperimentalAPI
val KoinModule = module { val KoinModule = module {
single { Configuration() }
// SQL connection // SQL connection
single { single {
val config: Configuration = get()
Connection( Connection(
host = Configuration.Database.host, host = config.database.host,
port = Configuration.Database.port, port = config.database.port,
database = Configuration.Database.database, database = config.database.database,
username = Configuration.Database.username, username = config.database.username,
password = Configuration.Database.password password = config.database.password
) )
} }
// Launch Database migration // Launch Database migration
single { Migrations(get(), Configuration.Sql.migrationFiles, Configuration.Sql.functionFiles) } single {
val config: Configuration = get()
Migrations(get(), config.sql.migrationFiles, config.sql.functionFiles)
}
// Redis client // Redis client
single<RedisClient> { single<RedisClient> {
RedisClient.create(Configuration.redis).apply { val config: Configuration = get()
RedisClient.create(config.redis).apply {
connect().sync().configSet("notify-keyspace-events", "KEA") connect().sync().configSet("notify-keyspace-events", "KEA")
} }
} }
single { NotificationsPush.Builder(get()) }
single {
val config: Configuration = get()
NotificationConsumer(get(), get(), get(), get(), get(), config.exchangeNotificationName)
}
// RabbitMQ // RabbitMQ
single<ConnectionFactory> { single<ConnectionFactory> {
ConnectionFactory().apply { setUri(Configuration.rabbitmq) } val config: Configuration = get()
ConnectionFactory().apply { setUri(config.rabbitmq) }
} }
// JsonSerializer // JsonSerializer
@@ -71,16 +88,26 @@ val KoinModule = module {
// SQL Requester (postgresJson) // SQL Requester (postgresJson)
single { single {
val config: Configuration = get()
Requester.RequesterFactory( Requester.RequesterFactory(
connection = get(), connection = get(),
functionsDirectory = Configuration.Sql.functionFiles functionsDirectory = config.sql.functionFiles
).createRequester() ).createRequester()
} }
// Mailer // Mailer
single { Mailer(Configuration.sendGridKey) } single {
val config: Configuration = get()
single { Publisher(factory = get(), exchangeName = Configuration.exchangeNotificationName) } Mailer(config.sendGridKey)
}
single { NotificationEmailSender(get<Mailer>(), Configuration.domain, get(), get()) }
single {
val config: Configuration = get()
Publisher(factory = get(), exchangeName = config.exchangeNotificationName)
}
single {
val config: Configuration = get()
NotificationEmailSender(get<Mailer>(), config.domain, get(), get())
}
} }

View File

@@ -7,5 +7,8 @@ import org.koin.dsl.module
val authKoinModule = module { val authKoinModule = module {
single { UserRepository(get()) } single { UserRepository(get()) }
// Used to send a connexion link by email // Used to send a connexion link by email
single { PasswordlessAuth(get<Mailer>(), Configuration.domain, get()) } single {
val config: Configuration = get()
PasswordlessAuth(get<Mailer>(), config.domain, get())
}
} }

View File

@@ -8,12 +8,15 @@ import org.elasticsearch.client.RestClient
import org.koin.dsl.module import org.koin.dsl.module
val viewKoinModule = module { val viewKoinModule = module {
single {
val config: Configuration = get()
// Elasticsearch Client // Elasticsearch Client
val esClient = RestClient.builder( val esClient = RestClient.builder(
HttpHost.create(Configuration.elasticsearch) HttpHost.create(config.elasticsearch)
).build().apply { ).build().apply {
createEsIndexForViews() createEsIndexForViews()
} }
ArticleViewManager<ArticleForView>(esClient)
single { ArticleViewManager<ArticleForView>(esClient) } }
} }

View File

@@ -10,16 +10,16 @@ import com.fasterxml.jackson.module.kotlin.readValue
import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleForView
import fr.postgresjson.entity.UuidEntity import fr.postgresjson.entity.UuidEntity
import org.joda.time.DateTime import org.joda.time.DateTime
import kotlin.random.Random import java.util.concurrent.atomic.AtomicInteger
open class Notification( open class Notification(
val type: String, val type: String,
val createdAt: DateTime = DateTime.now() val createdAt: DateTime = DateTime.now()
) { ) {
val id: Double = randId(createdAt.millis) val id: Double = nextId()
private fun randId(time: Long): Double { private fun nextId(): Double {
return (time.toString() + Random.nextInt(1000, 9999).toString()).toDouble() return (createdAt.millis.toString() + nextInt().toString()).toDouble()
} }
override fun toString(): String = mapper.writeValueAsString(this) ?: error("Unable to serialize notification") override fun toString(): String = mapper.writeValueAsString(this) ?: error("Unable to serialize notification")
@@ -27,6 +27,12 @@ open class Notification(
fun toByteArray() = toString().toByteArray() fun toByteArray() = toString().toByteArray()
companion object { companion object {
private val counter: AtomicInteger = AtomicInteger(1000)
fun nextInt(): Int {
counter.compareAndSet(9999, 1000)
return counter.incrementAndGet()
}
val mapper = jacksonObjectMapper().apply { val mapper = jacksonObjectMapper().apply {
registerModule(SimpleModule()) registerModule(SimpleModule())
propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE

View File

@@ -29,10 +29,18 @@ class NotificationConsumer(
private val notificationEmailSender: NotificationEmailSender, private val notificationEmailSender: NotificationEmailSender,
private val exchangeName: String, private val exchangeName: String,
) { ) {
val redis: RedisAsyncCommands<String, String> = redisClient.connect()?.async() ?: error("Unable to connect to redis") private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
private val rabbitConnection = rabbitFactory.newConnection()
private val rabbitChannel = rabbitConnection.createChannel()
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName) private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
fun config() { fun close() {
rabbitChannel.close()
rabbitConnection.close()
}
fun start() {
/* Config Rabbit */ /* Config Rabbit */
rabbitFactory.newConnection().use { connection -> rabbitFactory.newConnection().use { connection ->
connection.createChannel().use { channel -> connection.createChannel().use { channel ->
@@ -45,8 +53,6 @@ class NotificationConsumer(
} }
/* Define Consumer */ /* Define Consumer */
val rabbitChannel = rabbitFactory.newConnection().createChannel()
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) { val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class) @Throws(IOException::class)
override fun handleDelivery( override fun handleDelivery(

View File

@@ -1,34 +1,80 @@
package notification package notification
import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.core.JsonProcessingException
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.citizen.CitizenI import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.notification.Notification import fr.dcproject.notification.Notification
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.Frame.Text
import io.ktor.http.cio.websocket.readText
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.websocket.DefaultWebSocketServerSession
import io.lettuce.core.Limit import io.lettuce.core.Limit
import io.lettuce.core.Range import io.lettuce.core.Range
import io.lettuce.core.Range.Boundary import io.lettuce.core.Range.Boundary
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.lettuce.core.api.async.RedisAsyncCommands import io.lettuce.core.api.async.RedisAsyncCommands
import io.lettuce.core.pubsub.RedisPubSubAdapter import io.lettuce.core.pubsub.RedisPubSubAdapter
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
class NotificationsPush ( class NotificationsPush private constructor(
val redisClient: RedisClient, private val redis: RedisAsyncCommands<String, String>,
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
citizen: CitizenI, citizen: CitizenI,
incoming: Flow<Notification>, incoming: Flow<Notification>,
onRecieve: suspend (Notification) -> Unit, onRecieve: suspend (Notification) -> Unit,
) ) {
{ class Builder(val redisClient: RedisClient) {
val redis: RedisAsyncCommands<String, String> = redisClient.connect()?.async() ?: error("Unable to connect to redis") private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
val key = "notification:${citizen.id}" private val redisConnectionPubSub = redisClient.connectPubSub() ?: error("Unable to connect to redis")
private val redis: RedisAsyncCommands<String, String> = redisConnection.async() ?: error("Unable to connect to redis")
fun build(
citizen: CitizenI,
incoming: Flow<Notification>,
onRecieve: suspend (Notification) -> Unit,
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve)
@ExperimentalCoroutinesApi
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
/* Convert channel of string from websocket, to a flow of Notification object */
val incomingFlow: Flow<Notification> = ws.incoming.consumeAsFlow()
.mapNotNull<Frame, Text> { it as? Frame.Text }
.map { it.readText() }
.map { Notification.fromString(it) }
return build(ws.call.citizen, incomingFlow) {
ws.outgoing.send(Text(it.toString()))
}.apply {
ws.outgoing.invokeOnClose { close() }
}
}
}
private val key = "notification:${citizen.id}"
private var score: Double = 0.0 private var score: Double = 0.0
private val listener = object : RedisPubSubAdapter<String, String>() {
/* On new key publish */
override fun message(pattern: String?, channel: String?, message: String?) {
runBlocking {
getNotifications().collect {
onRecieve(it)
}
}
}
}
init { init {
/* Mark as read all incoming notifications */ /* Mark as read all incoming notifications */
@@ -44,21 +90,16 @@ class NotificationsPush (
} }
/* Lisen redis event, and sent the new notification into websocket */ /* Lisen redis event, and sent the new notification into websocket */
redisClient.connectPubSub()?.run { redisConnectionPubSub.run {
addListener(object : RedisPubSubAdapter<String, String>() { addListener(listener)
/* On new key publish */
override fun message(pattern: String?, channel: String?, message: String?) {
runBlocking {
getNotifications().collect {
onRecieve(it)
}
}
}
})
/* Register to the events */ /* Register to the events */
async()?.psubscribe("__key*__:$key") ?: error("Unable to connect to redis") async()?.psubscribe("__key*__:$key") ?: error("Unable to connect to redis")
} ?: error("PubSub Fail") }
}
fun close() {
redisConnectionPubSub.removeListener(listener)
} }
/* Return flow with all new notifications */ /* Return flow with all new notifications */

View File

@@ -1,19 +1,9 @@
package fr.dcproject.routes package fr.dcproject.routes
import fr.dcproject.component.auth.citizen
import fr.dcproject.notification.Notification
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.Frame.Text
import io.ktor.http.cio.websocket.readText
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.websocket.webSocket import io.ktor.websocket.webSocket
import io.lettuce.core.RedisClient
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import notification.NotificationsPush import notification.NotificationsPush
/** /**
@@ -23,18 +13,8 @@ import notification.NotificationsPush
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.notificationArticle(redisClient: RedisClient) { fun Route.notificationArticle(pushBuilder: NotificationsPush.Builder) {
webSocket("/notifications") { webSocket("/notifications") {
/* Convert channel of string from websocket, to a flow of Notification object */ pushBuilder.build(this)
val incomingFlow: Flow<Notification> = incoming.consumeAsFlow()
.mapNotNull<Frame, Text> { it as? Frame.Text }
.map { it.readText() }
.map { Notification.fromString(it) }
/* Read user notifications in redis then sent it to the websocket */
NotificationsPush(redisClient, call.citizen, incomingFlow) {
outgoing.send(Text(it.toString()))
} }
} }
}

View File

@@ -1,3 +1,5 @@
import com.rabbitmq.client.Channel
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.application.Configuration import fr.dcproject.application.Configuration
import fr.dcproject.application.Env.CUCUMBER import fr.dcproject.application.Env.CUCUMBER
import fr.dcproject.application.module import fr.dcproject.application.module
@@ -10,6 +12,8 @@ import io.cucumber.java8.Scenario
import io.cucumber.junit.Cucumber import io.cucumber.junit.Cucumber
import io.cucumber.junit.CucumberOptions import io.cucumber.junit.CucumberOptions
import io.ktor.server.testing.withTestApplication import io.ktor.server.testing.withTestApplication
import io.lettuce.core.RedisClient
import io.lettuce.core.api.sync.RedisCommands
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.test.KoinTest import org.koin.test.KoinTest
@@ -24,6 +28,12 @@ var unitialized: Boolean = false
@CucumberOptions(plugin = ["pretty"], strict = true) @CucumberOptions(plugin = ["pretty"], strict = true)
class CucumberTest : En, KoinTest { class CucumberTest : En, KoinTest {
private val logger: Logger? by LoggerDelegate() private val logger: Logger? by LoggerDelegate()
val config = Configuration("application-test.conf")
val redis: RedisCommands<String, String> = RedisClient.create(config.redis).connect().sync()
val rabbit: Channel = ConnectionFactory()
.apply { setUri(config.rabbitmq) }
.newConnection()
.createChannel()
@InternalCoroutinesApi @InternalCoroutinesApi
val ktorContext = KtorServerContext { val ktorContext = KtorServerContext {
@@ -47,6 +57,14 @@ class CucumberTest : En, KoinTest {
After { _: Scenario -> After { _: Scenario ->
//language=PostgreSQL //language=PostgreSQL
get<Connection>().sendQuery("rollback;", listOf()) get<Connection>().sendQuery("rollback;", listOf())
redis.flushall()
/* Purge rabbit notification queues */
rabbit.run {
queuePurge("push")
queuePurge("email")
}
ktorContext.stop() ktorContext.stop()
} }
} }
@@ -75,7 +93,7 @@ class CucumberTest : En, KoinTest {
private fun getFixturesRequester(): Requester { private fun getFixturesRequester(): Requester {
return Requester.RequesterFactory( return Requester.RequesterFactory(
connection = get(), connection = get(),
queriesDirectory = Configuration.Sql.fixtureFiles queriesDirectory = config.sql.fixtureFiles
).createRequester() ).createRequester()
} }
} }

View File

@@ -11,6 +11,7 @@ import io.ktor.server.testing.withTestApplication
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.koin.test.AutoCloseKoinTest import org.koin.test.AutoCloseKoinTest
@@ -20,10 +21,11 @@ import org.koin.test.get
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI @KtorExperimentalAPI
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("functional"))
class MailerTest : KoinTest, AutoCloseKoinTest() { class MailerTest : KoinTest, AutoCloseKoinTest() {
@InternalCoroutinesApi @InternalCoroutinesApi
@Test @Test
@Tag("online, functional") @Tags(Tag("online"))
fun `can be send an email`() { fun `can be send an email`() {
withTestApplication({ module(TEST) }) { withTestApplication({ module(TEST) }) {
get<Mailer>().sendEmail { get<Mailer>().sendEmail {

View File

@@ -7,10 +7,10 @@ import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.CitizenRef import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.follow.FollowArticleRepository import fr.dcproject.component.follow.FollowArticleRepository
import fr.dcproject.component.follow.FollowSimple import fr.dcproject.component.follow.FollowSimple
import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.notification.ArticleUpdateNotification import fr.dcproject.notification.ArticleUpdateNotification
import fr.dcproject.notification.NotificationConsumer import fr.dcproject.notification.NotificationConsumer
import fr.dcproject.notification.publisher.Publisher import fr.dcproject.notification.publisher.Publisher
import fr.dcproject.messages.NotificationEmailSender
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
@@ -22,31 +22,53 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_METHOD)
@Tags(Tag("functional"))
class NotificationConsumerTest { class NotificationConsumerTest {
companion object {
@BeforeAll
@JvmStatic
fun before() {
val config: Configuration = Configuration("application-test.conf")
RedisClient.create(config.redis).connect().sync().flushall()
/* Purge rabbit notification queues */
ConnectionFactory()
.apply { setUri(config.rabbitmq) }
.run {
newConnection().createChannel().apply {
queuePurge("push")
queuePurge("email")
}
}
}
}
@InternalCoroutinesApi @InternalCoroutinesApi
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI @KtorExperimentalAPI
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Test @Test
@Tag("functional")
fun `can be send notification`() = runBlocking { fun `can be send notification`() = runBlocking {
val config: Configuration = Configuration("application-test.conf")
/* Create mocks and spy's */ /* Create mocks and spy's */
val emailSender = mockk<NotificationEmailSender>() { val emailSender = mockk<NotificationEmailSender>() {
every { sendEmail(any()) } returns Unit every { sendEmail(any()) } returns Unit
} }
/* Init Spy on redis client */ /* Init Spy on redis client */
val redisClient = spyk<RedisClient>(RedisClient.create(Configuration.redis)) val redisClient = spyk<RedisClient>(RedisClient.create(config.redis))
val asyncCommand = spyk(redisClient.connect().async()) val asyncCommand = spyk(redisClient.connect().async())
every { redisClient.connect().async() } returns asyncCommand every { redisClient.connect().async() } returns asyncCommand
val rabbitFactory: ConnectionFactory = spyk { val rabbitFactory: ConnectionFactory = spyk {
ConnectionFactory().apply { setUri(Configuration.rabbitmq) } ConnectionFactory().apply { setUri(config.rabbitmq) }
} }
val followArticleRepo = mockk<FollowArticleRepository> { val followArticleRepo = mockk<FollowArticleRepository> {
every { findFollowsByTarget(any()) } returns flow { every { findFollowsByTarget(any()) } returns flow {
@@ -57,21 +79,15 @@ class NotificationConsumerTest {
} }
} }
/* Purge rabbit notification queues */
rabbitFactory.newConnection().createChannel().apply {
queuePurge("push")
queuePurge("email")
}
/* Config consumer */ /* Config consumer */
NotificationConsumer( val consumer = NotificationConsumer(
rabbitFactory = rabbitFactory, rabbitFactory = rabbitFactory,
redisClient = redisClient, redisClient = redisClient,
followArticleRepo = followArticleRepo, followArticleRepo = followArticleRepo,
followConstitutionRepo = mockk(), followConstitutionRepo = mockk(),
notificationEmailSender = emailSender, notificationEmailSender = emailSender,
exchangeName = "notification_test", exchangeName = "notification_test",
).config() ).apply { start() }
verify { rabbitFactory.newConnection() } verify { rabbitFactory.newConnection() }
/* Push message */ /* Push message */
@@ -93,5 +109,7 @@ class NotificationConsumerTest {
verify(timeout = 1000) { followArticleRepo.findFollowsByTarget(any()) } verify(timeout = 1000) { followArticleRepo.findFollowsByTarget(any()) }
verify(timeout = 1000) { emailSender.sendEmail(any()) } verify(timeout = 1000) { emailSender.sendEmail(any()) }
verify(timeout = 1000) { asyncCommand.zadd(any<String>(), any<Double>(), any<String>()) } verify(timeout = 1000) { asyncCommand.zadd(any<String>(), any<Double>(), any<String>()) }
// consumer.close()
} }
} }

View File

@@ -1,35 +1,54 @@
package functional package functional
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.application.Configuration import fr.dcproject.application.Configuration
import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.citizen.CitizenRef import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.notification.ArticleUpdateNotification import fr.dcproject.notification.ArticleUpdateNotification
import fr.dcproject.notification.Notification import fr.dcproject.notification.Notification
import io.lettuce.core.Limit
import io.lettuce.core.RedisClient import io.lettuce.core.RedisClient
import io.mockk.every import io.mockk.every
import io.mockk.spyk import io.mockk.spyk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import notification.NotificationsPush import notification.NotificationsPush
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.koin.test.AutoCloseKoinTest import kotlin.test.assertEquals
import org.koin.test.KoinTest
@Tags(Tag("functional"))
internal class NotificationsPushTest { internal class NotificationsPushTest {
companion object {
@BeforeAll
@JvmStatic
fun before() {
val config: Configuration = Configuration("application-test.conf")
RedisClient.create(config.redis).connect().sync().flushall()
/* Purge rabbit notification queues */
ConnectionFactory()
.apply { setUri(config.rabbitmq) }
.newConnection().createChannel().apply {
queuePurge("push")
queuePurge("email")
}
}
}
@Test @Test
@Tag("functional")
fun `Notification from redis is well catch and return`() = runBlocking { fun `Notification from redis is well catch and return`() = runBlocking {
val config: Configuration = Configuration("application-test.conf")
/* Redis client for test */ /* Redis client for test */
val redisClientTest = RedisClient.create(Configuration.redis) val redisClientTest = RedisClient.create(config.redis)
/* Init Spy on redis client */ /* Init Spy on redis client */
val redisClient = spyk<RedisClient>(RedisClient.create(Configuration.redis)) val redisClient = spyk<RedisClient>(RedisClient.create(config.redis))
val asyncCommand = spyk(redisClient.connect().async()) val asyncCommand = spyk(redisClient.connect().async())
every { redisClient.connect().async() } returns asyncCommand every { redisClient.connect().async() } returns asyncCommand
@@ -44,6 +63,9 @@ internal class NotificationsPushTest {
) )
/* Init two notification, one called before subscription, and the other after */ /* Init two notification, one called before subscription, and the other after */
val notifBeforeSubscribe = ArticleUpdateNotification(article) val notifBeforeSubscribe = ArticleUpdateNotification(article)
runBlocking {
delay(100)
}
val notifAfterSubscribe = ArticleUpdateNotification(article) val notifAfterSubscribe = ArticleUpdateNotification(article)
/* init event for emulate incomint message from websocket */ /* init event for emulate incomint message from websocket */
@@ -52,28 +74,30 @@ internal class NotificationsPushTest {
spyk(object { var counter = 0 }).run { /* Counter for count the callback of notification */ spyk(object { var counter = 0 }).run { /* Counter for count the callback of notification */
/* Sent notification */ /* Sent notification */
redisClientTest.connect().sync().run { redisClientTest.connect().run {
zadd( sync().zadd(
"notification:${citizen.id}", "notification:${citizen.id}",
notifBeforeSubscribe.id, notifBeforeSubscribe.id,
notifBeforeSubscribe.toString() notifBeforeSubscribe.toString()
) )
close()
} }
/* Init NotificationPush system, and set assertion in callback */ /* Init NotificationPush system, and set assertion in callback */
NotificationsPush(redisClient, citizen, incomingFlow) { val notificationPush = NotificationsPush.Builder(redisClient).build(citizen, incomingFlow) {
counter++ counter++
if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id if (counter == 1) it.id `should be equal to` notifBeforeSubscribe.id
else it.id `should be equal to` notifAfterSubscribe.id else it.id `should be equal to` notifAfterSubscribe.id
} }
/* Sent the notification */ /* Sent the notification */
redisClientTest.connect().sync().run { redisClientTest.connect().run {
zadd( sync().zadd(
"notification:${citizen.id}", "notification:${citizen.id}",
notifAfterSubscribe.id, notifAfterSubscribe.id,
notifAfterSubscribe.toString() notifAfterSubscribe.toString()
) )
close()
} }
/* Verify if the callback is called 2 times */ /* Verify if the callback is called 2 times */
@@ -83,7 +107,8 @@ internal class NotificationsPushTest {
/* Emit an event to delete notification */ /* Emit an event to delete notification */
event.emit(notifAfterSubscribe) event.emit(notifAfterSubscribe)
/* Verify the "mark as read" is called */ /* Verify the "mark as read" is called */
verify(timeout = 300) { asyncCommand.zremrangebyscore(any(), any()) } verify(timeout = 500) { asyncCommand.zremrangebyscore(any(), any()) }
notificationPush.close()
} }
} }
} }

View File

@@ -1,11 +1,14 @@
package functional package functional
import fr.dcproject.utils.readResource import fr.dcproject.utils.readResource
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import kotlin.test.assertEquals import kotlin.test.assertEquals
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tags(Tag("functional"))
class ResourcesKtTest { class ResourcesKtTest {
@Test @Test
fun readResource() { fun readResource() {

View File

@@ -10,6 +10,7 @@ import io.ktor.server.testing.withTestApplication
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
@@ -19,7 +20,7 @@ import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@KtorExperimentalAPI @KtorExperimentalAPI
@TestInstance(PER_CLASS) @TestInstance(PER_CLASS)
@Tag("functional") @Tags(Tag("functional"))
class ViewTest { class ViewTest {
@Test @Test
fun `test View Article`() { fun `test View Article`() {

View File

@@ -14,6 +14,7 @@ import io.mockk.mockk
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
@@ -23,7 +24,7 @@ import fr.dcproject.component.article.ArticleRepository as ArticleRepo
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tag("security") @Tags(Tag("security"), Tag("unit"))
internal class ArticleAccessControlTest { internal class ArticleAccessControlTest {
private val tesla = CitizenCart( private val tesla = CitizenCart(
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"), id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),

View File

@@ -10,6 +10,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
@@ -17,7 +18,7 @@ import org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tag("security") @Tags(Tag("security"), Tag("unit"))
internal class CitizenAccessControlTest { internal class CitizenAccessControlTest {
private val tesla = CitizenBasic( private val tesla = CitizenBasic(
user = User( user = User(

View File

@@ -15,6 +15,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
@@ -23,7 +24,7 @@ import java.util.UUID
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tag("security") @Tags(Tag("security"), Tag("unit"))
internal class CommentAccessControlTest { internal class CommentAccessControlTest {
private val tesla = Citizen( private val tesla = Citizen(
user = User( user = User(

View File

@@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
@@ -22,7 +23,7 @@ import java.util.UUID
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tag("security") @Tags(Tag("security"), Tag("unit"))
internal class FollowAccessControlTest { internal class FollowAccessControlTest {
private val tesla = CitizenBasic( private val tesla = CitizenBasic(
user = User( user = User(

View File

@@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
@@ -22,7 +23,7 @@ import java.util.UUID
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tag("security") @Tags(Tag("security"), Tag("unit"))
internal class OpinionAccessControlTest { internal class OpinionAccessControlTest {
private val tesla = CitizenBasic( private val tesla = CitizenBasic(
user = User( user = User(

View File

@@ -12,6 +12,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
@@ -20,7 +21,7 @@ import java.util.UUID
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tag("security") @Tags(Tag("security"), Tag("unit"))
internal class OpinionChoiceAccessControlTest { internal class OpinionChoiceAccessControlTest {
private val tesla = CitizenBasic( private val tesla = CitizenBasic(
id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"), id = UUID.fromString("e6efc288-4283-4729-a268-6debb18de1a0"),

View File

@@ -14,6 +14,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
@@ -23,7 +24,7 @@ import fr.dcproject.component.vote.entity.Vote as VoteEntity
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tag("security") @Tags(Tag("security"), Tag("unit"))
internal class VoteAccessControlTest { internal class VoteAccessControlTest {
private val tesla = Citizen( private val tesla = Citizen(
id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"), id = UUID.fromString("a1e35c99-9d33-4fb4-9201-58d7071243bb"),

View File

@@ -12,6 +12,7 @@ import fr.dcproject.security.AccessDecision.GRANTED
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.joda.time.DateTime import org.joda.time.DateTime
import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.Execution
@@ -21,7 +22,7 @@ import fr.dcproject.component.workgroup.Workgroup as WorkgroupEntity
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(CONCURRENT) @Execution(CONCURRENT)
@Tag("security") @Tags(Tag("security"), Tag("unit"))
internal class WorkgroupAccessControlTest { internal class WorkgroupAccessControlTest {
private val tesla = CitizenBasic( private val tesla = CitizenBasic(
user = User( user = User(

View File

@@ -9,7 +9,7 @@ ktor {
} }
app { app {
envName = prod envName = test
domain = dc-project.fr domain = dc-project.fr
} }