Skip to content

Commit

Permalink
chore: add some missing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ttypic committed Dec 3, 2024
1 parent e37d4d4 commit 4078b7c
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 5 deletions.
4 changes: 3 additions & 1 deletion chat-android/src/main/java/com/ably/chat/Connection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.ably.lib.realtime.ConnectionState
import io.ably.lib.types.ErrorInfo
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -114,9 +115,10 @@ interface Connection {
internal class DefaultConnection(
pubSubConnection: PubSubConnection,
private val logger: Logger,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
) : Connection {

private val connectionScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1) + SupervisorJob())
private val connectionScope = CoroutineScope(dispatcher.limitedParallelism(1) + SupervisorJob())

private val listeners: MutableList<Connection.Listener> = CopyOnWriteArrayList()

Expand Down
4 changes: 3 additions & 1 deletion chat-android/src/main/java/com/ably/chat/Typing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.ably.lib.types.ErrorInfo
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.math.min
import kotlin.math.pow
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -92,6 +93,7 @@ data class TypingEvent(val currentlyTyping: Set<String>)

internal class DefaultTyping(
private val room: DefaultRoom,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
) : Typing, ContributesToRoomLifecycleImpl(room.roomLogger) {
private val typingIndicatorsChannelName = "${room.roomId}::\$chat::\$typingIndicators"

Expand All @@ -103,7 +105,7 @@ internal class DefaultTyping(

private val logger = room.roomLogger.withContext(tag = "Typing")

private val typingScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1) + SupervisorJob())
private val typingScope = CoroutineScope(dispatcher.limitedParallelism(1) + SupervisorJob())

private val eventBus = MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
Expand Down
95 changes: 92 additions & 3 deletions chat-android/src/test/java/com/ably/chat/ConnectionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import io.ably.lib.types.ErrorInfo
import io.mockk.every
import io.mockk.slot
import io.mockk.spyk
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
Expand All @@ -19,15 +22,12 @@ class ConnectionTest {

private val pubSubConnection = spyk<PubSubConnection>(buildRealtimeConnection())

private lateinit var connection: Connection

private val pubSubConnectionStateListenerSlot = slot<ConnectionStateListener>()

@Before
fun setUp() {
every { pubSubConnection.on(capture(pubSubConnectionStateListenerSlot)) } returns Unit
pubSubConnection.state = ConnectionState.initialized
connection = DefaultConnection(pubSubConnection, EmptyLogger(LogContext(tag = "TEST")))
}

/**
Expand All @@ -48,6 +48,7 @@ class ConnectionTest {
*/
@Test
fun `status update events must contain the newly entered connection status`() = runTest {
val connection = DefaultConnection(pubSubConnection, EmptyLogger(LogContext(tag = "TEST")))
val deferredEvent = CompletableDeferred<ConnectionStatusChange>()

connection.onStatusChange {
Expand All @@ -73,4 +74,92 @@ class ConnectionTest {
deferredEvent.await(),
)
}

/**
* @spec: CHA-CS5a1
*/
@Test
fun `should wait 5 sec if the connection status transitions from CONNECTED to DISCONNECTED`() = runTest {
val testScheduler = TestCoroutineScheduler()
val dispatcher = StandardTestDispatcher(testScheduler)
val connection = DefaultConnection(pubSubConnection, EmptyLogger(LogContext(tag = "TEST")), dispatcher)

var status = ConnectionStatus.Initialized

connection.onStatusChange {
status = it.current
}

fireConnected()

testScheduler.runCurrent()

assertEquals(ConnectionStatus.Connected, status)

fireDisconnected()

testScheduler.runCurrent()

assertEquals(ConnectionStatus.Connected, status)

testScheduler.advanceTimeBy(5000.milliseconds)
testScheduler.runCurrent()

assertEquals(ConnectionStatus.Disconnected, status)
}

/**
* @spec: CHA-CS5a3
*/
@Test
fun `should cancel the transient disconnect timer IF realtime connections status changes to CONNECTED`() = runTest {
val testScheduler = TestCoroutineScheduler()
val dispatcher = StandardTestDispatcher(testScheduler)
val connection = DefaultConnection(pubSubConnection, EmptyLogger(LogContext(tag = "TEST")), dispatcher)

var status = ConnectionStatus.Initialized

connection.onStatusChange {
status = it.current
}

fireConnected()

testScheduler.runCurrent()

assertEquals(ConnectionStatus.Connected, status)

fireDisconnected()

testScheduler.runCurrent()

assertEquals(ConnectionStatus.Connected, status)

testScheduler.advanceTimeBy(3000.milliseconds)

fireConnected()

testScheduler.advanceTimeBy(5000.milliseconds)
testScheduler.runCurrent()

assertEquals(ConnectionStatus.Connected, status)
}

private fun fireConnected() = pubSubConnectionStateListenerSlot.captured.onConnectionStateChanged(
ConnectionStateChange(
ConnectionState.initialized,
ConnectionState.connected,
0,
null,
),
)

private fun fireDisconnected() = pubSubConnectionStateListenerSlot.captured.onConnectionStateChanged(
ConnectionStateChange(
ConnectionState.initialized,
ConnectionState.connected,
0,
null,
),
)
}
41 changes: 41 additions & 0 deletions chat-android/src/test/java/com/ably/chat/SandboxTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,47 @@ class SandboxTest {
)
}

@Test
fun `should observe room reactions`() = runTest {
val chatClient = sandbox.createSandboxChatClient()
val roomId = UUID.randomUUID().toString()
val roomOptions = RoomOptions(reactions = RoomReactionsOptions)

val room = chatClient.rooms.get(roomId, roomOptions)
room.attach()

val reactionEvent = CompletableDeferred<Reaction>()

room.reactions.subscribe { reactionEvent.complete(it) }

room.reactions.send("heart")

assertEquals(
"heart",
reactionEvent.await().type,
)
}

@Test
fun `should be able to send and retrieve messages`() = runTest {
val chatClient = sandbox.createSandboxChatClient()
val roomId = UUID.randomUUID().toString()

val room = chatClient.rooms.get(roomId)

room.attach()

val messageEvent = CompletableDeferred<MessageEvent>()

room.messages.subscribe { messageEvent.complete(it) }
room.messages.send("hello")

assertEquals(
"hello",
messageEvent.await().message.text,
)
}

companion object {

private lateinit var sandbox: Sandbox
Expand Down
135 changes: 135 additions & 0 deletions chat-android/src/test/java/com/ably/chat/TypingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.ably.chat

import com.ably.chat.room.DEFAULT_CLIENT_ID
import com.ably.chat.room.createMockChannel
import com.ably.chat.room.createMockChatApi
import com.ably.chat.room.createMockRealtimeClient
import com.ably.chat.room.createMockRoom
import io.ably.lib.realtime.CompletionListener
import io.ably.lib.realtime.Presence
import io.ably.lib.types.ChannelOptions
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test

class TypingTest {

private lateinit var room: DefaultRoom
private val pubSubPresence = mockk<Presence>(relaxed = true)

@Before
fun setUp() {
val realtimeClient = createMockRealtimeClient()
val mockRealtimeChannel = realtimeClient.createMockChannel("room1::\$chat::\$messages")
mockRealtimeChannel.setPrivateField("presence", pubSubPresence)

every { realtimeClient.channels.get(any<String>(), any<ChannelOptions>()) } returns mockRealtimeChannel
every { pubSubPresence.enterClient(DEFAULT_CLIENT_ID, any(), any()) } answers {
val completionListener = arg<CompletionListener>(2)
completionListener.onSuccess()
}

val mockChatApi = createMockChatApi(realtimeClient)
room = spyk(createMockRoom("room1", realtimeClient = realtimeClient, chatApi = mockChatApi))

coEvery { room.ensureAttached() } returns Unit
}

/**
* @spec CHA-T4a1
*/
@Test
fun `when a typing session is started, the client is entered into presence on the typing channel`() = runTest {
val typing = DefaultTyping(room)
typing.start()
verify(exactly = 1) { pubSubPresence.enterClient("clientId", any(), any()) }
}

/**
* @spec CHA-T4a2
*/
@Test
fun `when timeout expires, the typing session is automatically ended by leaving presence`() = runTest {
val testScheduler = TestCoroutineScheduler()
val dispatcher = StandardTestDispatcher(testScheduler)
val scope = CoroutineScope(dispatcher)
val typing = DefaultTyping(room, dispatcher)

scope.launch {
typing.start()
}

testScheduler.advanceTimeBy(5000.milliseconds)
testScheduler.runCurrent()

verify(exactly = 1) { pubSubPresence.enterClient("clientId", any(), any()) }
verify(exactly = 1) { pubSubPresence.leaveClient("clientId", any(), any()) }
}

/**
* @spec CHA-T4b
*/
@Test
fun `if typing is already in progress, the timeout is extended to be timeoutMs from now`() = runTest {
val testScheduler = TestCoroutineScheduler()
val dispatcher = StandardTestDispatcher(testScheduler)
val scope = CoroutineScope(dispatcher)
val typing = DefaultTyping(room, dispatcher)

scope.launch {
typing.start()
}

testScheduler.advanceTimeBy(3000.milliseconds)
testScheduler.runCurrent()

scope.launch {
typing.start()
}

testScheduler.advanceTimeBy(3000.milliseconds)
testScheduler.runCurrent()

verify(exactly = 1) { pubSubPresence.enterClient("clientId", any(), any()) }
verify(exactly = 0) { pubSubPresence.leaveClient("clientId", any(), any()) }
}

/**
* @spec CHA-T5b
*/
@Test
fun `if typing is in progress, the timeout is cancelled, the client then leaves presence`() = runTest {
val testScheduler = TestCoroutineScheduler()
val dispatcher = StandardTestDispatcher(testScheduler)
val scope = CoroutineScope(dispatcher)
val typing = DefaultTyping(room, dispatcher)

scope.launch {
typing.start()
}

testScheduler.advanceTimeBy(1000.milliseconds)
testScheduler.runCurrent()

verify(exactly = 1) { pubSubPresence.enterClient("clientId", any(), any()) }

scope.launch {
typing.stop()
}

testScheduler.advanceTimeBy(5000.milliseconds)
testScheduler.runCurrent()

verify(exactly = 1) { pubSubPresence.leaveClient("clientId", any(), any()) }
}
}

0 comments on commit 4078b7c

Please sign in to comment.