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:
2021-04-14 01:41:49 +02:00
parent 50b4cf1816
commit 39c665b7a9
9 changed files with 130 additions and 13 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View 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())
}
}
}
}
}
}

View File

@@ -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
}

View File

@@ -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()))
} }

View File

@@ -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';