From 8f32f319151352836d477a700a9a03a87f437aa7 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 9 Jul 2024 18:22:07 +0200 Subject: [PATCH 1/5] Initial commit --- Realtime/build.gradle.kts | 7 ++++ .../github/jan/supabase/realtime/Realtime.kt | 2 ++ .../jan/supabase/realtime/RealtimeImpl.kt | 2 +- .../src/commonTest/kotlin/RealtimeTest.kt | 33 +++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 Realtime/src/commonTest/kotlin/RealtimeTest.kt diff --git a/Realtime/build.gradle.kts b/Realtime/build.gradle.kts index 4dfd1297..9810b901 100644 --- a/Realtime/build.gradle.kts +++ b/Realtime/build.gradle.kts @@ -21,6 +21,13 @@ kotlin { api(libs.ktor.client.websockets) } } + val commonTest by getting { + dependencies { + implementation("io.ktor:ktor-server-test-host:2.3.12") + implementation(project(":test-common")) + implementation(libs.bundles.testing) + } + } } } diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt index c8760ec0..c993f203 100644 --- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt +++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt @@ -12,6 +12,7 @@ import io.github.jan.supabase.plugins.MainPlugin import io.github.jan.supabase.plugins.SupabasePluginProvider import io.github.jan.supabase.serializer.KotlinXSerializer import io.github.jan.supabase.supabaseJson +import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession import io.ktor.client.plugins.websocket.WebSockets import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter import kotlinx.coroutines.flow.StateFlow @@ -113,6 +114,7 @@ sealed interface Realtime : MainPlugin, CustomSerializationPlug var disconnectOnSessionLoss: Boolean = true, var connectOnSubscribe: Boolean = true, var disconnectOnNoSubscriptions: Boolean = true, + var websocketSessionProvider: (suspend () -> DefaultClientWebSocketSession)? = null, @Deprecated("This property is deprecated and will be removed in a future version.") var eventsPerSecond: Int = 10, ): MainConfig(), CustomSerializationConfig { diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt index 0f0b5f31..e5c05da5 100644 --- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt +++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt @@ -76,7 +76,7 @@ import kotlin.time.Duration.Companion.milliseconds _status.value = Realtime.Status.CONNECTING val realtimeUrl = websocketUrl try { - ws = supabaseClient.httpClient.webSocketSession(realtimeUrl) + ws = config.websocketSessionProvider?.invoke() ?: supabaseClient.httpClient.webSocketSession(realtimeUrl) _status.value = Realtime.Status.CONNECTED Realtime.logger.i { "Connected to realtime websocket!" } listenForMessages() diff --git a/Realtime/src/commonTest/kotlin/RealtimeTest.kt b/Realtime/src/commonTest/kotlin/RealtimeTest.kt new file mode 100644 index 00000000..b86e105d --- /dev/null +++ b/Realtime/src/commonTest/kotlin/RealtimeTest.kt @@ -0,0 +1,33 @@ +import io.github.jan.supabase.createSupabaseClient +import io.github.jan.supabase.logging.LogLevel +import io.github.jan.supabase.realtime.Realtime +import io.github.jan.supabase.realtime.realtime +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocket +import io.ktor.server.testing.testApplication +import kotlin.test.Test + +class RealtimeTest { + + @Test + fun test() { + testApplication { + val client = createClient { + install(WebSockets) + } + client.webSocket("/") { + val supabase = createSupabaseClient("", "") { + defaultLogLevel = LogLevel.DEBUG + install(Realtime) { + websocketSessionProvider = { + this@webSocket + } + } + } + supabase.realtime.connect() + //send(Json.encodeToString(RealtimeMessage("topic", "event", buildJsonObject { }, null))) + } + } + } + +} \ No newline at end of file From 40955e8e1d3156630b0b89dfb8f91e20234d7036 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 10 Jul 2024 20:55:04 +0200 Subject: [PATCH 2/5] Add first tests --- Realtime/build.gradle.kts | 3 +- .../jan/supabase/realtime/RealtimeImpl.kt | 32 +++--- .../commonTest/kotlin/RealtimeChannelTest.kt | 107 ++++++++++++++++++ .../src/commonTest/kotlin/RealtimeTest.kt | 46 ++++---- .../src/commonTest/kotlin/RealtimeWSMock.kt | 52 +++++++++ gradle/libs.versions.toml | 2 + 6 files changed, 201 insertions(+), 41 deletions(-) create mode 100644 Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt create mode 100644 Realtime/src/commonTest/kotlin/RealtimeWSMock.kt diff --git a/Realtime/build.gradle.kts b/Realtime/build.gradle.kts index 9810b901..0fb87c02 100644 --- a/Realtime/build.gradle.kts +++ b/Realtime/build.gradle.kts @@ -23,7 +23,8 @@ kotlin { } val commonTest by getting { dependencies { - implementation("io.ktor:ktor-server-test-host:2.3.12") + implementation(libs.ktor.server.host) + implementation(libs.ktor.server.websockets) implementation(project(":test-common")) implementation(libs.bundles.testing) } diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt index e5c05da5..57c576e7 100644 --- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt +++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt @@ -13,29 +13,27 @@ import io.github.jan.supabase.logging.e import io.github.jan.supabase.logging.i import io.github.jan.supabase.logging.w import io.github.jan.supabase.supabaseJson -import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession -import io.ktor.client.plugins.websocket.sendSerialized -import io.ktor.client.statement.HttpResponse -import io.ktor.http.URLProtocol -import io.ktor.http.path -import io.ktor.websocket.Frame -import io.ktor.websocket.readText +import io.ktor.client.plugins.websocket.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.websocket.* import kotlinx.atomicfu.atomic -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.job -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.json.buildJsonObject +import kotlin.collections.Map +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.filter +import kotlin.collections.forEach +import kotlin.collections.listOf +import kotlin.collections.plus +import kotlin.collections.set +import kotlin.collections.toMap import kotlin.time.Duration.Companion.milliseconds @PublishedApi internal class RealtimeImpl(override val supabaseClient: SupabaseClient, override val config: Realtime.Config) : Realtime { @@ -234,7 +232,7 @@ import kotlin.time.Duration.Companion.milliseconds } override suspend fun close() { - ws?.cancel() + disconnect() } override suspend fun block() { diff --git a/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt b/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt new file mode 100644 index 00000000..73fc4a36 --- /dev/null +++ b/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt @@ -0,0 +1,107 @@ +import io.github.jan.supabase.gotrue.Auth +import io.github.jan.supabase.gotrue.auth +import io.github.jan.supabase.gotrue.minimalSettings +import io.github.jan.supabase.realtime.RealtimeJoinPayload +import io.github.jan.supabase.realtime.RealtimeMessage +import io.github.jan.supabase.realtime.channel +import io.ktor.server.websocket.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonPrimitive +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class RealtimeChannelTest { + + @Test + fun testConnectOnSubscribe() { + createTestClient( + wsHandler = { + //Does not matter for this test + }, + supabaseHandler = { + val channel = it.channel("") + assertFailsWith() { + channel.subscribe() + } + }, + realtimeConfig = { + connectOnSubscribe = false + } + ) + } + + @Test + fun testSendingPayloadWithoutJWT() { + val expectedChannelId = "channelId" + val expectedIsPrivate = true + val expectedReceiveOwnBroadcasts = true + val expectedAcknowledge = true + val expectedPresenceKey = "presenceKey" + createTestClient( + wsHandler = { + val message = this.receiveDeserialized() + val payload = Json.decodeFromJsonElement(message.payload) + assertEquals("realtime:$expectedChannelId", message.topic) + assertEquals(expectedIsPrivate, payload.config.isPrivate) + assertEquals(expectedReceiveOwnBroadcasts, payload.config.broadcast.receiveOwnBroadcasts) + assertEquals(expectedAcknowledge, payload.config.broadcast.acknowledgeBroadcasts) + assertEquals(expectedPresenceKey, payload.config.presence.key) + }, + supabaseHandler = { + val channel = it.channel("channelId") { + isPrivate = expectedIsPrivate + broadcast { + receiveOwnBroadcasts = expectedReceiveOwnBroadcasts + acknowledgeBroadcasts = expectedAcknowledge + } + presence { + key = expectedPresenceKey + } + } + channel.subscribe() + } + ) + } + + @Test + fun testSendingPayloadWithAuthJWT() { + val expectedAuthToken = "authToken" + createTestClient( + wsHandler = { + val message = this.receiveDeserialized() + assertEquals(expectedAuthToken, message.payload["access_token"]?.jsonPrimitive?.content) + }, + supabaseHandler = { + it.auth.importAuthToken(expectedAuthToken) + val channel = it.channel("channelId") + channel.subscribe() + }, + supabaseConfig = { + install(Auth) { + minimalSettings() + } + } + ) + } + + @Test + fun testSendingPayloadWithCustomJWT() { + val expectedAuthToken = "authToken" + createTestClient( + wsHandler = { + val message = this.receiveDeserialized() + assertEquals(expectedAuthToken, message.payload["access_token"]?.jsonPrimitive?.content) + }, + supabaseHandler = { + val channel = it.channel("channelId") + channel.subscribe() + }, + realtimeConfig = { + jwtToken = expectedAuthToken + } + ) + } + +} \ No newline at end of file diff --git a/Realtime/src/commonTest/kotlin/RealtimeTest.kt b/Realtime/src/commonTest/kotlin/RealtimeTest.kt index b86e105d..b0d19e33 100644 --- a/Realtime/src/commonTest/kotlin/RealtimeTest.kt +++ b/Realtime/src/commonTest/kotlin/RealtimeTest.kt @@ -1,33 +1,33 @@ -import io.github.jan.supabase.createSupabaseClient -import io.github.jan.supabase.logging.LogLevel -import io.github.jan.supabase.realtime.Realtime +import io.github.jan.supabase.realtime.RealtimeMessage import io.github.jan.supabase.realtime.realtime -import io.ktor.client.plugins.websocket.WebSockets -import io.ktor.client.plugins.websocket.webSocket -import io.ktor.server.testing.testApplication +import io.ktor.server.websocket.* +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import kotlin.test.Test +import kotlin.test.assertEquals class RealtimeTest { @Test - fun test() { - testApplication { - val client = createClient { - install(WebSockets) + fun testSendingRealtimeMessages() { + val expectedMessage = RealtimeMessage( + topic = "realtimeTopic", + event = "realtimeEvent", + payload = buildJsonObject { + put("key", "value") + }, + ref = "realtimeRef" + ) + createTestClient( + wsHandler = { + val message = this.receiveDeserialized() + assertEquals(expectedMessage, message) + }, + supabaseHandler = { + it.realtime.connect() + it.realtime.send(expectedMessage) } - client.webSocket("/") { - val supabase = createSupabaseClient("", "") { - defaultLogLevel = LogLevel.DEBUG - install(Realtime) { - websocketSessionProvider = { - this@webSocket - } - } - } - supabase.realtime.connect() - //send(Json.encodeToString(RealtimeMessage("topic", "event", buildJsonObject { }, null))) - } - } + ) } } \ No newline at end of file diff --git a/Realtime/src/commonTest/kotlin/RealtimeWSMock.kt b/Realtime/src/commonTest/kotlin/RealtimeWSMock.kt new file mode 100644 index 00000000..77250988 --- /dev/null +++ b/Realtime/src/commonTest/kotlin/RealtimeWSMock.kt @@ -0,0 +1,52 @@ +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.SupabaseClientBuilder +import io.github.jan.supabase.createSupabaseClient +import io.github.jan.supabase.logging.LogLevel +import io.github.jan.supabase.realtime.Realtime +import io.github.jan.supabase.supabaseJson +import io.ktor.client.plugins.websocket.* +import io.ktor.serialization.kotlinx.* +import io.ktor.server.testing.* +import io.ktor.server.websocket.* +import io.ktor.server.websocket.WebSockets + +fun ApplicationTestBuilder.configureServer( + handler: suspend DefaultWebSocketServerSession.() -> Unit +) { + install(WebSockets) { + contentConverter = KotlinxWebsocketSerializationConverter(supabaseJson) + } + routing { + webSocket("/", handler = handler) + } +} + +fun createTestClient( + wsHandler: suspend DefaultWebSocketServerSession.() -> Unit, + supabaseHandler: suspend (SupabaseClient) -> Unit, + realtimeConfig: Realtime.Config.() -> Unit = {}, + supabaseConfig: SupabaseClientBuilder.() -> Unit = {} +) { + testApplication { + configureServer(wsHandler) + val client = createClient { + install(io.ktor.client.plugins.websocket.WebSockets) { + contentConverter = KotlinxWebsocketSerializationConverter(supabaseJson) + } + } + client.webSocket("/") { + val supabase = createSupabaseClient("", "") { + defaultLogLevel = LogLevel.DEBUG + install(Realtime) { + websocketSessionProvider = { + this@webSocket + } + realtimeConfig() + } + supabaseConfig() + } + supabaseHandler(supabase) + supabase.close() + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ff9c9d50..617ba4ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,6 +74,8 @@ ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.re ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } ktor-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } +ktor-server-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } +ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" } ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } From 34217077cf46966f9f1011a34ae0aba1294a878f Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 10 Jul 2024 20:58:43 +0200 Subject: [PATCH 3/5] remove wildcard imports --- .../jan/supabase/realtime/RealtimeImpl.kt | 27 +++++++++++-------- .../commonTest/kotlin/RealtimeChannelTest.kt | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt index 57c576e7..556aa1a2 100644 --- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt +++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt @@ -13,27 +13,32 @@ import io.github.jan.supabase.logging.e import io.github.jan.supabase.logging.i import io.github.jan.supabase.logging.w import io.github.jan.supabase.supabaseJson -import io.ktor.client.plugins.websocket.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.websocket.* +import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession +import io.ktor.client.plugins.websocket.sendSerialized +import io.ktor.client.statement.HttpResponse +import io.ktor.http.URLProtocol +import io.ktor.http.path +import io.ktor.websocket.Frame +import io.ktor.websocket.readText import kotlinx.atomicfu.atomic -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.job +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.json.buildJsonObject -import kotlin.collections.Map import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.filter -import kotlin.collections.forEach -import kotlin.collections.listOf -import kotlin.collections.plus import kotlin.collections.set -import kotlin.collections.toMap import kotlin.time.Duration.Companion.milliseconds @PublishedApi internal class RealtimeImpl(override val supabaseClient: SupabaseClient, override val config: Realtime.Config) : Realtime { diff --git a/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt b/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt index 73fc4a36..4d6b9415 100644 --- a/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt +++ b/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt @@ -4,7 +4,7 @@ import io.github.jan.supabase.gotrue.minimalSettings import io.github.jan.supabase.realtime.RealtimeJoinPayload import io.github.jan.supabase.realtime.RealtimeMessage import io.github.jan.supabase.realtime.channel -import io.ktor.server.websocket.* +import io.ktor.server.websocket.receiveDeserialized import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.jsonPrimitive From d9bd0d438572534993c92f26f6f84acc090e5edb Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 10 Jul 2024 20:59:05 +0200 Subject: [PATCH 4/5] remove more wildcard imports --- Realtime/src/commonTest/kotlin/RealtimeTest.kt | 2 +- Realtime/src/commonTest/kotlin/RealtimeWSMock.kt | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Realtime/src/commonTest/kotlin/RealtimeTest.kt b/Realtime/src/commonTest/kotlin/RealtimeTest.kt index b0d19e33..32990c35 100644 --- a/Realtime/src/commonTest/kotlin/RealtimeTest.kt +++ b/Realtime/src/commonTest/kotlin/RealtimeTest.kt @@ -1,6 +1,6 @@ import io.github.jan.supabase.realtime.RealtimeMessage import io.github.jan.supabase.realtime.realtime -import io.ktor.server.websocket.* +import io.ktor.server.websocket.receiveDeserialized import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlin.test.Test diff --git a/Realtime/src/commonTest/kotlin/RealtimeWSMock.kt b/Realtime/src/commonTest/kotlin/RealtimeWSMock.kt index 77250988..25ca2dc2 100644 --- a/Realtime/src/commonTest/kotlin/RealtimeWSMock.kt +++ b/Realtime/src/commonTest/kotlin/RealtimeWSMock.kt @@ -4,11 +4,13 @@ import io.github.jan.supabase.createSupabaseClient import io.github.jan.supabase.logging.LogLevel import io.github.jan.supabase.realtime.Realtime import io.github.jan.supabase.supabaseJson -import io.ktor.client.plugins.websocket.* -import io.ktor.serialization.kotlinx.* -import io.ktor.server.testing.* -import io.ktor.server.websocket.* +import io.ktor.client.plugins.websocket.webSocket +import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter +import io.ktor.server.testing.ApplicationTestBuilder +import io.ktor.server.testing.testApplication +import io.ktor.server.websocket.DefaultWebSocketServerSession import io.ktor.server.websocket.WebSockets +import io.ktor.server.websocket.webSocket fun ApplicationTestBuilder.configureServer( handler: suspend DefaultWebSocketServerSession.() -> Unit From a70b37eddcdc743a8cf05c9d85b7c2d1370086c5 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 11 Jul 2024 13:05:25 +0200 Subject: [PATCH 5/5] Add status tests and initial presence & broadcast sending tests --- Realtime/src/commonTest/kotlin/FlowUtils.kt | 5 ++ .../commonTest/kotlin/RealtimeChannelTest.kt | 88 ++++++++++++++++++- .../src/commonTest/kotlin/RealtimeTest.kt | 17 ++++ .../commonTest/kotlin/RealtimeTestUtils.kt | 11 +++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 Realtime/src/commonTest/kotlin/FlowUtils.kt create mode 100644 Realtime/src/commonTest/kotlin/RealtimeTestUtils.kt diff --git a/Realtime/src/commonTest/kotlin/FlowUtils.kt b/Realtime/src/commonTest/kotlin/FlowUtils.kt new file mode 100644 index 00000000..7088900a --- /dev/null +++ b/Realtime/src/commonTest/kotlin/FlowUtils.kt @@ -0,0 +1,5 @@ +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first + +suspend inline fun Flow.waitForValue(value: T) = filter { it == value }.first() \ No newline at end of file diff --git a/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt b/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt index 4d6b9415..f7edc6b1 100644 --- a/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt +++ b/Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt @@ -1,13 +1,22 @@ import io.github.jan.supabase.gotrue.Auth import io.github.jan.supabase.gotrue.auth import io.github.jan.supabase.gotrue.minimalSettings +import io.github.jan.supabase.realtime.Realtime +import io.github.jan.supabase.realtime.RealtimeChannel +import io.github.jan.supabase.realtime.RealtimeChannel.Companion.CHANNEL_EVENT_REPLY +import io.github.jan.supabase.realtime.RealtimeChannel.Companion.CHANNEL_EVENT_SYSTEM import io.github.jan.supabase.realtime.RealtimeJoinPayload import io.github.jan.supabase.realtime.RealtimeMessage import io.github.jan.supabase.realtime.channel +import io.github.jan.supabase.realtime.realtime import io.ktor.server.websocket.receiveDeserialized +import io.ktor.server.websocket.sendSerialized import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -15,7 +24,7 @@ import kotlin.test.assertFailsWith class RealtimeChannelTest { @Test - fun testConnectOnSubscribe() { + fun testConnectOnSubscribeDisabled() { createTestClient( wsHandler = { //Does not matter for this test @@ -32,6 +41,43 @@ class RealtimeChannelTest { ) } + @Test + fun testConnectOnSubscribeEnabled() { + createTestClient( + wsHandler = { + incoming.receive() + }, + supabaseHandler = { + val channel = it.channel("") + channel.subscribe(false) + assertEquals(Realtime.Status.CONNECTED, it.realtime.status.value) + } + ) + } + + @Test + fun testChannelStatusWithoutPostgres() { + val channelId = "channelId" + createTestClient( + wsHandler = { + incoming.receive() + sendSerialized(RealtimeMessage("realtime:$channelId", CHANNEL_EVENT_SYSTEM, buildJsonObject { put("status", "ok") }, "")) + incoming.receive() + sendSerialized(RealtimeMessage("realtime:$channelId", CHANNEL_EVENT_REPLY, buildJsonObject { put("status", "ok") }, "")) + }, + supabaseHandler = { + val channel = it.channel("channelId") + assertEquals(channel.status.value, RealtimeChannel.Status.UNSUBSCRIBED) + assertEquals(it.realtime.status.value, Realtime.Status.DISCONNECTED) + channel.subscribe(blockUntilSubscribed = true) + assertEquals(channel.status.value, RealtimeChannel.Status.SUBSCRIBED) + channel.unsubscribe() + assertEquals(channel.status.value, RealtimeChannel.Status.UNSUBSCRIBING) + assertEquals(RealtimeChannel.Status.UNSUBSCRIBED, channel.status.waitForValue(RealtimeChannel.Status.UNSUBSCRIBED)) + }, + ) + } + @Test fun testSendingPayloadWithoutJWT() { val expectedChannelId = "channelId" @@ -104,4 +150,44 @@ class RealtimeChannelTest { ) } + @Test + fun testSendingBroadcasts() { + val message = buildJsonObject { + put("key", "value") + } + val event = "event" + createTestClient( + wsHandler = { + handleSubscribe("channelId") + val rMessage = this.receiveDeserialized() + assertEquals("realtime:channelId", rMessage.topic) + assertEquals("broadcast", rMessage.event) + assertEquals(message, rMessage.payload["payload"]?.jsonObject) + assertEquals(event, rMessage.payload["event"]?.jsonPrimitive?.content) + assertEquals("broadcast", rMessage.payload["type"]?.jsonPrimitive?.content) + }, + supabaseHandler = { + val channel = it.channel("channelId") + channel.subscribe(true) + channel.broadcast(event, message) + } + ) + } + + @Test + fun testSendingPresenceUnsubscribed() { + createTestClient( + wsHandler = { + handleSubscribe("channelId") + }, + supabaseHandler = { + val channel = it.channel("channelId") + channel.subscribe(true) + assertFailsWith { + channel.track(buildJsonObject { }) + } + } + ) + } + } \ No newline at end of file diff --git a/Realtime/src/commonTest/kotlin/RealtimeTest.kt b/Realtime/src/commonTest/kotlin/RealtimeTest.kt index 32990c35..689c465d 100644 --- a/Realtime/src/commonTest/kotlin/RealtimeTest.kt +++ b/Realtime/src/commonTest/kotlin/RealtimeTest.kt @@ -1,3 +1,4 @@ +import io.github.jan.supabase.realtime.Realtime import io.github.jan.supabase.realtime.RealtimeMessage import io.github.jan.supabase.realtime.realtime import io.ktor.server.websocket.receiveDeserialized @@ -7,6 +8,22 @@ import kotlin.test.Test import kotlin.test.assertEquals class RealtimeTest { + + @Test + fun testRealtimeStatus() { + createTestClient( + wsHandler = { + //Does not matter for this test + }, + supabaseHandler = { + assertEquals(Realtime.Status.DISCONNECTED, it.realtime.status.value) + it.realtime.connect() + assertEquals(Realtime.Status.CONNECTED, it.realtime.status.value) + it.realtime.disconnect() + assertEquals(Realtime.Status.DISCONNECTED, it.realtime.status.value) + } + ) + } @Test fun testSendingRealtimeMessages() { diff --git a/Realtime/src/commonTest/kotlin/RealtimeTestUtils.kt b/Realtime/src/commonTest/kotlin/RealtimeTestUtils.kt new file mode 100644 index 00000000..00bd10a7 --- /dev/null +++ b/Realtime/src/commonTest/kotlin/RealtimeTestUtils.kt @@ -0,0 +1,11 @@ +import io.github.jan.supabase.realtime.RealtimeChannel.Companion.CHANNEL_EVENT_SYSTEM +import io.github.jan.supabase.realtime.RealtimeMessage +import io.ktor.server.websocket.DefaultWebSocketServerSession +import io.ktor.server.websocket.sendSerialized +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +suspend fun DefaultWebSocketServerSession.handleSubscribe(channelId: String) { + incoming.receive() + sendSerialized(RealtimeMessage("realtime:$channelId", CHANNEL_EVENT_SYSTEM, buildJsonObject { put("status", "ok") }, "")) +} \ No newline at end of file