Add Test for Notification routes
Add @JsonSubTypes on Notification return all creator on request find_follows_article_by_target Add testNotifications task
This commit is contained in:
@@ -350,6 +350,12 @@ tasks.register("testFollows", Test::class) {
|
|||||||
includeTags("follow")
|
includeTags("follow")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tasks.register("testNotifications", Test::class) {
|
||||||
|
group = "tests"
|
||||||
|
useJUnitPlatform {
|
||||||
|
includeTags("notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencyCheck {
|
dependencyCheck {
|
||||||
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
|
formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package fr.dcproject.component.notification
|
package fr.dcproject.component.notification
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
@@ -12,6 +14,10 @@ import fr.dcproject.component.article.database.ArticleForView
|
|||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
|
||||||
|
@JsonSubTypes(
|
||||||
|
JsonSubTypes.Type(value = ArticleUpdateNotification::class, name = "article")
|
||||||
|
)
|
||||||
open class Notification(
|
open class Notification(
|
||||||
val type: String,
|
val type: String,
|
||||||
val createdAt: DateTime = DateTime.now()
|
val createdAt: DateTime = DateTime.now()
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class NotificationsPush private constructor(
|
class NotificationsPush(
|
||||||
private val redis: RedisAsyncCommands<String, String>,
|
private val redis: RedisAsyncCommands<String, String>,
|
||||||
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
|
private val redisConnectionPubSub: StatefulRedisPubSubConnection<String, String>,
|
||||||
citizen: CitizenI,
|
citizen: CitizenI,
|
||||||
incoming: Flow<Notification>,
|
incoming: Flow<Notification>,
|
||||||
onRecieve: suspend (Notification) -> Unit,
|
onReceive: suspend (Notification) -> Unit,
|
||||||
) {
|
) {
|
||||||
class Builder(val redisClient: RedisClient) {
|
class Builder(val redisClient: RedisClient) {
|
||||||
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
|
private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis")
|
||||||
@@ -43,8 +43,8 @@ class NotificationsPush private constructor(
|
|||||||
fun build(
|
fun build(
|
||||||
citizen: CitizenI,
|
citizen: CitizenI,
|
||||||
incoming: Flow<Notification>,
|
incoming: Flow<Notification>,
|
||||||
onRecieve: suspend (Notification) -> Unit,
|
onReceive: suspend (Notification) -> Unit,
|
||||||
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve)
|
): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onReceive)
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
|
fun build(ws: DefaultWebSocketServerSession): NotificationsPush {
|
||||||
@@ -69,7 +69,7 @@ class NotificationsPush private constructor(
|
|||||||
override fun message(pattern: String?, channel: String?, message: String?) {
|
override fun message(pattern: String?, channel: String?, message: String?) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getNotifications().collect {
|
getNotifications().collect {
|
||||||
onRecieve(it)
|
onReceive(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,10 +85,12 @@ class NotificationsPush private constructor(
|
|||||||
|
|
||||||
/* Get old notification and sent it to websocket */
|
/* Get old notification and sent it to websocket */
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getNotifications().collect { onRecieve(it) }
|
getNotifications().collect {
|
||||||
|
onReceive(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lisen redis event, and sent the new notification into websocket */
|
/* Listen redis event, and sent the new notification into websocket */
|
||||||
redisConnectionPubSub.run {
|
redisConnectionPubSub.run {
|
||||||
addListener(listener)
|
addListener(listener)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ begin
|
|||||||
f.created_at,
|
f.created_at,
|
||||||
f.target_reference,
|
f.target_reference,
|
||||||
json_build_object('id', f.target_id) as target,
|
json_build_object('id', f.target_id) as target,
|
||||||
json_build_object('id', f.created_by_id) as created_by
|
find_citizen_by_id_with_user(f.created_by_id) as created_by
|
||||||
from follow_article as f
|
from follow_article as f
|
||||||
join article a on f.target_id = a.id
|
join article a on f.target_id = a.id
|
||||||
where a.version_id = _version_id
|
where a.version_id = _version_id
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ internal class NotificationsPushTest {
|
|||||||
@BeforeAll
|
@BeforeAll
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun before() {
|
fun before() {
|
||||||
val config: Configuration = Configuration("application-test.conf")
|
val config = Configuration("application-test.conf")
|
||||||
RedisClient.create(config.redis).connect().sync().flushall()
|
RedisClient.create(config.redis).connect().sync().flushall()
|
||||||
|
|
||||||
/* Purge rabbit notification queues */
|
/* Purge rabbit notification queues */
|
||||||
@@ -45,7 +45,7 @@ internal class NotificationsPushTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
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")
|
val config = Configuration("application-test.conf")
|
||||||
/* Redis client for test */
|
/* Redis client for test */
|
||||||
val redisClientTest = RedisClient.create(config.redis)
|
val redisClientTest = RedisClient.create(config.redis)
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ internal class NotificationsPushTest {
|
|||||||
}
|
}
|
||||||
val notifAfterSubscribe = ArticleUpdateNotification(article)
|
val notifAfterSubscribe = ArticleUpdateNotification(article)
|
||||||
|
|
||||||
/* init event for emulate incomint message from websocket */
|
/* init event for emulate incoming message from websocket */
|
||||||
val event = MutableSharedFlow<Notification>()
|
val event = MutableSharedFlow<Notification>()
|
||||||
val incomingFlow = event.asSharedFlow()
|
val incomingFlow = event.asSharedFlow()
|
||||||
|
|
||||||
|
|||||||
78
src/test/kotlin/integration/Notification routes.kt
Normal file
78
src/test/kotlin/integration/Notification routes.kt
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import fr.dcproject.common.utils.toUUID
|
||||||
|
import fr.dcproject.component.article.database.ArticleForView
|
||||||
|
import fr.dcproject.component.auth.database.UserCreator
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenCreator
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenI.Name
|
||||||
|
import fr.dcproject.component.notification.ArticleUpdateNotification
|
||||||
|
import fr.dcproject.component.notification.Notification
|
||||||
|
import fr.dcproject.component.notification.Publisher
|
||||||
|
import integration.steps.given.`Given I have article`
|
||||||
|
import integration.steps.given.`Given I have citizen`
|
||||||
|
import integration.steps.given.`Given I have follow on article`
|
||||||
|
import integration.steps.given.`authenticated in url as`
|
||||||
|
import io.ktor.http.cio.websocket.Frame
|
||||||
|
import io.ktor.http.cio.websocket.readText
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.junit.jupiter.api.Tag
|
||||||
|
import org.junit.jupiter.api.Tags
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.koin.test.get
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
@Tags(Tag("integration"), Tag("notification"))
|
||||||
|
class `Notification routes` : BaseTest() {
|
||||||
|
@Test
|
||||||
|
fun `I can send notification`() {
|
||||||
|
withIntegrationApplication {
|
||||||
|
`Given I have citizen`("John", "Doe", id = "1a34191a-9cde-45ba-8ac1-230138a102d3")
|
||||||
|
`Given I have article`(id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", createdBy = Name(firstName = "John", lastName = "Doe"))
|
||||||
|
`Given I have follow on article`("John", "Doe", article = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4")
|
||||||
|
val notification = ArticleUpdateNotification(
|
||||||
|
ArticleForView(
|
||||||
|
id = "a06cbfb7-3094-4d64-aaa1-7486c0c292f4".toUUID(),
|
||||||
|
title = "MyTitle",
|
||||||
|
content = "myContent",
|
||||||
|
description = "myDescription",
|
||||||
|
createdBy = CitizenCreator(
|
||||||
|
id = "1a34191a-9cde-45ba-8ac1-230138a102d3".toUUID(),
|
||||||
|
name = Name(firstName = "John", lastName = "Doe"),
|
||||||
|
email = "john-doe@plop.com",
|
||||||
|
user = UserCreator(username = "john-doe"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val publisher = get<Publisher>()
|
||||||
|
launch {
|
||||||
|
publisher
|
||||||
|
.publish(notification)
|
||||||
|
.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(1000)
|
||||||
|
|
||||||
|
handleWebSocketConversation(
|
||||||
|
"/notifications",
|
||||||
|
{
|
||||||
|
`authenticated in url as`("John", "Doe")
|
||||||
|
}
|
||||||
|
) { incoming, outgoing ->
|
||||||
|
incoming.receive().let {
|
||||||
|
when (it) {
|
||||||
|
is Frame.Text -> Notification.fromString<ArticleUpdateNotification>(it.readText()).let { notif ->
|
||||||
|
assertEquals(
|
||||||
|
"a06cbfb7-3094-4d64-aaa1-7486c0c292f4",
|
||||||
|
notif.target.id.toString()
|
||||||
|
)
|
||||||
|
outgoing.send(it)
|
||||||
|
}
|
||||||
|
else -> error(it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package integration.steps.given
|
|||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import fr.dcproject.component.auth.jwt.JwtConfig
|
import fr.dcproject.component.auth.jwt.JwtConfig
|
||||||
import fr.dcproject.component.citizen.database.Citizen
|
import fr.dcproject.component.citizen.database.Citizen
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenI
|
||||||
import fr.dcproject.component.citizen.database.CitizenRepository
|
import fr.dcproject.component.citizen.database.CitizenRepository
|
||||||
import io.ktor.http.HttpHeaders
|
import io.ktor.http.HttpHeaders
|
||||||
import io.ktor.server.testing.TestApplicationRequest
|
import io.ktor.server.testing.TestApplicationRequest
|
||||||
@@ -25,3 +26,23 @@ fun TestApplicationRequest.`authenticated as`(
|
|||||||
|
|
||||||
return citizen
|
return citizen
|
||||||
}
|
}
|
||||||
|
fun TestApplicationRequest.`authenticated in url as`(
|
||||||
|
firstName: String,
|
||||||
|
lastName: String,
|
||||||
|
): Citizen {
|
||||||
|
val repo: CitizenRepository by lazy<CitizenRepository> { GlobalContext.get().koin.get() }
|
||||||
|
val citizen = repo.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist with name $firstName $lastName")
|
||||||
|
val algorithm = GlobalContext.get().koin.get<JwtConfig>().algorithm
|
||||||
|
val jwtAsString: String = JWT.create()
|
||||||
|
.withIssuer("dc-project.fr")
|
||||||
|
.withClaim("id", citizen.user.id.toString())
|
||||||
|
.sign(algorithm)
|
||||||
|
|
||||||
|
uri += when (uri.contains('?')) {
|
||||||
|
true -> '&'
|
||||||
|
false -> '?'
|
||||||
|
}
|
||||||
|
uri += "token=$jwtAsString"
|
||||||
|
|
||||||
|
return citizen
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package integration.steps.given
|
|||||||
import fr.dcproject.common.utils.toUUID
|
import fr.dcproject.common.utils.toUUID
|
||||||
import fr.dcproject.component.article.database.ArticleRef
|
import fr.dcproject.component.article.database.ArticleRef
|
||||||
import fr.dcproject.component.citizen.database.Citizen
|
import fr.dcproject.component.citizen.database.Citizen
|
||||||
|
import fr.dcproject.component.citizen.database.CitizenI
|
||||||
import fr.dcproject.component.citizen.database.CitizenRef
|
import fr.dcproject.component.citizen.database.CitizenRef
|
||||||
import fr.dcproject.component.citizen.database.CitizenRepository
|
import fr.dcproject.component.citizen.database.CitizenRepository
|
||||||
import fr.dcproject.component.constitution.database.ConstitutionRef
|
import fr.dcproject.component.constitution.database.ConstitutionRef
|
||||||
@@ -30,7 +31,7 @@ fun TestApplicationEngine.`Given I have follow on article`(
|
|||||||
article: String,
|
article: String,
|
||||||
) {
|
) {
|
||||||
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
|
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
|
||||||
val citizen = citizenRepository.findByUsername("$firstName-$lastName".toLowerCase()) ?: error("Citizen not exist")
|
val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist")
|
||||||
createFollow(citizen, ArticleRef(article.toUUID()))
|
createFollow(citizen, ArticleRef(article.toUUID()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ fun TestApplicationEngine.`Given I have follow on constitution`(
|
|||||||
constitution: String,
|
constitution: String,
|
||||||
) {
|
) {
|
||||||
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
|
val citizenRepository: CitizenRepository by lazy { GlobalContext.get().koin.get() }
|
||||||
val citizen = citizenRepository.findByUsername("$firstName-$lastName".toLowerCase()) ?: error("Citizen not exist")
|
val citizen = citizenRepository.findByName(CitizenI.Name(firstName, lastName)) ?: error("Citizen not exist")
|
||||||
createFollow(citizen, ArticleRef(constitution.toUUID()))
|
createFollow(citizen, ArticleRef(constitution.toUUID()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ begin
|
|||||||
assert (select following = true from find_follow(first_article_id, _citizen_id, 'article')), '(v1) find_follow must return the following';
|
assert (select following = true from find_follow(first_article_id, _citizen_id, 'article')), '(v1) find_follow must return the following';
|
||||||
assert (select following = true from find_follow(first_article_updated_id, _citizen_id, 'article')), '(v2) find_follow must return the following';
|
assert (select following = true from find_follow(first_article_updated_id, _citizen_id, 'article')), '(v2) find_follow must return the following';
|
||||||
|
|
||||||
|
assert (select f.total = 1 from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return 1 follow';
|
||||||
|
assert (select (f.resource#>>'{0, created_by, id}')::uuid = _citizen_id from find_follows_article_by_target(first_article_id) as f), 'find_follows_article_by_target must return follows with creator';
|
||||||
|
|
||||||
perform unfollow('article'::regclass, first_article_id, _citizen_id);
|
perform unfollow('article'::regclass, first_article_id, _citizen_id);
|
||||||
assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow, event if article is on other version';
|
assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow, event if article is on other version';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user