From 39c665b7a90b74ef1af7ca3b11af3d62204cc8b4 Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Wed, 14 Apr 2021 01:41:49 +0200 Subject: [PATCH] Add Test for Notification routes Add @JsonSubTypes on Notification return all creator on request find_follows_article_by_target Add testNotifications task --- build.gradle.kts | 6 ++ .../component/notification/Notification.kt | 6 ++ .../notification/NotificationsPush.kt | 16 ++-- .../follow/find_follows_article_by_target.sql | 2 +- .../functional/NotificationsPushTest.kt | 6 +- .../kotlin/integration/Notification routes.kt | 78 +++++++++++++++++++ .../kotlin/integration/steps/given/Auth.kt | 21 +++++ .../kotlin/integration/steps/given/Follow.kt | 5 +- src/test/resources/sql/follow.sql | 3 + 9 files changed, 130 insertions(+), 13 deletions(-) create mode 100644 src/test/kotlin/integration/Notification routes.kt diff --git a/build.gradle.kts b/build.gradle.kts index c227605..1d811c4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -350,6 +350,12 @@ tasks.register("testFollows", Test::class) { includeTags("follow") } } +tasks.register("testNotifications", Test::class) { + group = "tests" + useJUnitPlatform { + includeTags("notification") + } +} dependencyCheck { formats = listOf(ReportGenerator.Format.HTML, ReportGenerator.Format.XML) diff --git a/src/main/kotlin/fr/dcproject/component/notification/Notification.kt b/src/main/kotlin/fr/dcproject/component/notification/Notification.kt index ff8871c..57e4320 100644 --- a/src/main/kotlin/fr/dcproject/component/notification/Notification.kt +++ b/src/main/kotlin/fr/dcproject/component/notification/Notification.kt @@ -1,5 +1,7 @@ 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.PropertyNamingStrategies import com.fasterxml.jackson.databind.SerializationFeature @@ -12,6 +14,10 @@ import fr.dcproject.component.article.database.ArticleForView import org.joda.time.DateTime 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( val type: String, val createdAt: DateTime = DateTime.now() diff --git a/src/main/kotlin/fr/dcproject/component/notification/NotificationsPush.kt b/src/main/kotlin/fr/dcproject/component/notification/NotificationsPush.kt index 08e6e29..445cca1 100644 --- a/src/main/kotlin/fr/dcproject/component/notification/NotificationsPush.kt +++ b/src/main/kotlin/fr/dcproject/component/notification/NotificationsPush.kt @@ -28,12 +28,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory -class NotificationsPush private constructor( +class NotificationsPush( private val redis: RedisAsyncCommands, private val redisConnectionPubSub: StatefulRedisPubSubConnection, citizen: CitizenI, incoming: Flow, - onRecieve: suspend (Notification) -> Unit, + onReceive: suspend (Notification) -> Unit, ) { class Builder(val redisClient: RedisClient) { private val redisConnection = redisClient.connect() ?: error("Unable to connect to redis") @@ -43,8 +43,8 @@ class NotificationsPush private constructor( fun build( citizen: CitizenI, incoming: Flow, - onRecieve: suspend (Notification) -> Unit, - ): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onRecieve) + onReceive: suspend (Notification) -> Unit, + ): NotificationsPush = NotificationsPush(redis, redisConnectionPubSub, citizen, incoming, onReceive) @ExperimentalCoroutinesApi fun build(ws: DefaultWebSocketServerSession): NotificationsPush { @@ -69,7 +69,7 @@ class NotificationsPush private constructor( override fun message(pattern: String?, channel: String?, message: String?) { runBlocking { getNotifications().collect { - onRecieve(it) + onReceive(it) } } } @@ -85,10 +85,12 @@ class NotificationsPush private constructor( /* Get old notification and sent it to websocket */ 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 { addListener(listener) diff --git a/src/main/resources/sql/functions/follow/find_follows_article_by_target.sql b/src/main/resources/sql/functions/follow/find_follows_article_by_target.sql index fb08b76..6baded1 100644 --- a/src/main/resources/sql/functions/follow/find_follows_article_by_target.sql +++ b/src/main/resources/sql/functions/follow/find_follows_article_by_target.sql @@ -21,7 +21,7 @@ begin f.created_at, f.target_reference, 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 join article a on f.target_id = a.id where a.version_id = _version_id diff --git a/src/test/kotlin/functional/NotificationsPushTest.kt b/src/test/kotlin/functional/NotificationsPushTest.kt index cedbaf6..7464c6b 100644 --- a/src/test/kotlin/functional/NotificationsPushTest.kt +++ b/src/test/kotlin/functional/NotificationsPushTest.kt @@ -30,7 +30,7 @@ internal class NotificationsPushTest { @BeforeAll @JvmStatic fun before() { - val config: Configuration = Configuration("application-test.conf") + val config = Configuration("application-test.conf") RedisClient.create(config.redis).connect().sync().flushall() /* Purge rabbit notification queues */ @@ -45,7 +45,7 @@ internal class NotificationsPushTest { @Test 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 */ val redisClientTest = RedisClient.create(config.redis) @@ -74,7 +74,7 @@ internal class NotificationsPushTest { } val notifAfterSubscribe = ArticleUpdateNotification(article) - /* init event for emulate incomint message from websocket */ + /* init event for emulate incoming message from websocket */ val event = MutableSharedFlow() val incomingFlow = event.asSharedFlow() diff --git a/src/test/kotlin/integration/Notification routes.kt b/src/test/kotlin/integration/Notification routes.kt new file mode 100644 index 0000000..d50b298 --- /dev/null +++ b/src/test/kotlin/integration/Notification routes.kt @@ -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() + 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(it.readText()).let { notif -> + assertEquals( + "a06cbfb7-3094-4d64-aaa1-7486c0c292f4", + notif.target.id.toString() + ) + outgoing.send(it) + } + else -> error(it.toString()) + } + } + } + } + } +} diff --git a/src/test/kotlin/integration/steps/given/Auth.kt b/src/test/kotlin/integration/steps/given/Auth.kt index f8e1b8b..6c6fb71 100644 --- a/src/test/kotlin/integration/steps/given/Auth.kt +++ b/src/test/kotlin/integration/steps/given/Auth.kt @@ -3,6 +3,7 @@ package integration.steps.given import com.auth0.jwt.JWT import fr.dcproject.component.auth.jwt.JwtConfig import fr.dcproject.component.citizen.database.Citizen +import fr.dcproject.component.citizen.database.CitizenI import fr.dcproject.component.citizen.database.CitizenRepository import io.ktor.http.HttpHeaders import io.ktor.server.testing.TestApplicationRequest @@ -25,3 +26,23 @@ fun TestApplicationRequest.`authenticated as`( return citizen } +fun TestApplicationRequest.`authenticated in url as`( + firstName: String, + lastName: String, +): Citizen { + val repo: CitizenRepository by lazy { 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().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 +} diff --git a/src/test/kotlin/integration/steps/given/Follow.kt b/src/test/kotlin/integration/steps/given/Follow.kt index 084f09d..3e9191f 100644 --- a/src/test/kotlin/integration/steps/given/Follow.kt +++ b/src/test/kotlin/integration/steps/given/Follow.kt @@ -3,6 +3,7 @@ package integration.steps.given import fr.dcproject.common.utils.toUUID import fr.dcproject.component.article.database.ArticleRef 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.CitizenRepository import fr.dcproject.component.constitution.database.ConstitutionRef @@ -30,7 +31,7 @@ fun TestApplicationEngine.`Given I have follow on article`( article: String, ) { 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())) } @@ -40,7 +41,7 @@ fun TestApplicationEngine.`Given I have follow on constitution`( constitution: String, ) { 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())) } diff --git a/src/test/resources/sql/follow.sql b/src/test/resources/sql/follow.sql index 58c27bd..0d98afc 100644 --- a/src/test/resources/sql/follow.sql +++ b/src/test/resources/sql/follow.sql @@ -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_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); assert (select count(*) = 0 from follow), 'follow must be deleted after unfollow, event if article is on other version';