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