diff --git a/chat-android/src/main/java/com/ably/chat/Occupancy.kt b/chat-android/src/main/java/com/ably/chat/Occupancy.kt index f8c68876..c2afa229 100644 --- a/chat-android/src/main/java/com/ably/chat/Occupancy.kt +++ b/chat-android/src/main/java/com/ably/chat/Occupancy.kt @@ -4,7 +4,6 @@ package com.ably.chat import com.google.gson.JsonObject import com.google.gson.JsonPrimitive -import io.ably.lib.realtime.AblyRealtime import io.ably.lib.realtime.Channel import java.util.concurrent.CopyOnWriteArrayList import kotlinx.coroutines.CoroutineScope @@ -73,11 +72,8 @@ data class OccupancyEvent( ) internal class DefaultOccupancy( - realtimeChannels: AblyRealtime.Channels, - private val chatApi: ChatApi, - private val roomId: String, - private val logger: Logger, -) : Occupancy, ContributesToRoomLifecycleImpl(logger) { + private val room: DefaultRoom, +) : Occupancy, ContributesToRoomLifecycleImpl(room.roomLogger) { override val featureName: String = "occupancy" @@ -85,8 +81,12 @@ internal class DefaultOccupancy( override val detachmentErrorCode: ErrorCode = ErrorCode.OccupancyDetachmentFailed + private val realtimeChannels = room.realtimeClient.channels + + private val logger = room.roomLogger.withContext(tag = "Occupancy") + // (CHA-O1) - private val messagesChannelName = "$roomId::\$chat::\$chatMessages" + private val messagesChannelName = "${room.roomId}::\$chat::\$chatMessages" override val channel: Channel = realtimeChannels.get( messagesChannelName, @@ -142,7 +142,7 @@ internal class DefaultOccupancy( // (CHA-O3) override suspend fun get(): OccupancyEvent { logger.trace("Occupancy.get()") - return chatApi.getOccupancy(roomId) + return room.chatApi.getOccupancy(room.roomId) } override fun release() { diff --git a/chat-android/src/main/java/com/ably/chat/Presence.kt b/chat-android/src/main/java/com/ably/chat/Presence.kt index 278a8a37..0fa3faee 100644 --- a/chat-android/src/main/java/com/ably/chat/Presence.kt +++ b/chat-android/src/main/java/com/ably/chat/Presence.kt @@ -6,7 +6,6 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import io.ably.lib.realtime.Channel import io.ably.lib.types.PresenceMessage -import io.ably.lib.realtime.Presence as PubSubPresence import io.ably.lib.realtime.Presence.PresenceListener as PubSubPresenceListener typealias PresenceData = JsonElement @@ -134,11 +133,8 @@ data class PresenceEvent( ) internal class DefaultPresence( - private val clientId: String, - override val channel: Channel, - private val presence: PubSubPresence, - private val logger: Logger, -) : Presence, ContributesToRoomLifecycleImpl(logger) { + private val room: DefaultRoom, +) : Presence, ContributesToRoomLifecycleImpl(room.roomLogger) { override val featureName = "presence" @@ -146,7 +142,14 @@ internal class DefaultPresence( override val detachmentErrorCode: ErrorCode = ErrorCode.PresenceDetachmentFailed + override val channel: Channel = room.messages.channel + + private val logger = room.roomLogger.withContext(tag = "Presence") + + private val presence = channel.presence + override suspend fun get(waitForSync: Boolean, clientId: String?, connectionId: String?): List { + room.ensureAttached() return presence.getCoroutine(waitForSync, clientId, connectionId).map { user -> PresenceMember( clientId = user.clientId, @@ -160,15 +163,18 @@ internal class DefaultPresence( override suspend fun isUserPresent(clientId: String): Boolean = presence.getCoroutine(clientId = clientId).isNotEmpty() override suspend fun enter(data: PresenceData?) { - presence.enterClientCoroutine(clientId, wrapInUserCustomData(data)) + room.ensureAttached() + presence.enterClientCoroutine(room.clientId, wrapInUserCustomData(data)) } override suspend fun update(data: PresenceData?) { - presence.updateClientCoroutine(clientId, wrapInUserCustomData(data)) + room.ensureAttached() + presence.updateClientCoroutine(room.clientId, wrapInUserCustomData(data)) } override suspend fun leave(data: PresenceData?) { - presence.leaveClientCoroutine(clientId, wrapInUserCustomData(data)) + room.ensureAttached() + presence.leaveClientCoroutine(room.clientId, wrapInUserCustomData(data)) } override fun subscribe(listener: Presence.Listener): Subscription { diff --git a/chat-android/src/main/java/com/ably/chat/Room.kt b/chat-android/src/main/java/com/ably/chat/Room.kt index a262cc62..63777b4b 100644 --- a/chat-android/src/main/java/com/ably/chat/Room.kt +++ b/chat-android/src/main/java/com/ably/chat/Room.kt @@ -112,12 +112,12 @@ interface Room { internal class DefaultRoom( override val roomId: String, override val options: RoomOptions, - private val realtimeClient: RealtimeClient, - chatApi: ChatApi, - clientId: String, + internal val realtimeClient: RealtimeClient, + internal val chatApi: ChatApi, + internal val clientId: String, logger: Logger, ) : Room { - private val roomLogger = logger.withContext("Room", mapOf("roomId" to roomId)) + internal val roomLogger = logger.withContext("Room", mapOf("roomId" to roomId)) /** * RoomScope is a crucial part of the Room lifecycle. It manages sequential and atomic operations. @@ -186,46 +186,25 @@ internal class DefaultRoom( val roomFeatures = mutableListOf(messages) options.presence?.let { - val presenceContributor = DefaultPresence( - clientId = clientId, - channel = messages.channel, - presence = messages.channel.presence, - logger = roomLogger.withContext(tag = "Presence"), - ) + val presenceContributor = DefaultPresence(room = this) roomFeatures.add(presenceContributor) _presence = presenceContributor } options.typing?.let { - val typingContributor = DefaultTyping( - roomId = roomId, - realtimeClient = realtimeClient, - clientId = clientId, - options = options.typing, - logger = roomLogger.withContext(tag = "Typing"), - ) + val typingContributor = DefaultTyping(room = this) roomFeatures.add(typingContributor) _typing = typingContributor } options.reactions?.let { - val reactionsContributor = DefaultRoomReactions( - roomId = roomId, - clientId = clientId, - realtimeChannels = realtimeClient.channels, - logger = roomLogger.withContext(tag = "Reactions"), - ) + val reactionsContributor = DefaultRoomReactions(room = this) roomFeatures.add(reactionsContributor) _reactions = reactionsContributor } options.occupancy?.let { - val occupancyContributor = DefaultOccupancy( - roomId = roomId, - realtimeChannels = realtimeClient.channels, - chatApi = chatApi, - logger = roomLogger.withContext(tag = "Occupancy"), - ) + val occupancyContributor = DefaultOccupancy(room = this) roomFeatures.add(occupancyContributor) _occupancy = occupancyContributor } @@ -256,6 +235,11 @@ internal class DefaultRoom( lifecycleManager.release() } + /** + * Ensures that the room is attached before performing any realtime room operation. + * @throws roomInvalidStateException if room is not in ATTACHING/ATTACHED state. + * Spec: CHA-RL9 + */ internal suspend fun ensureAttached() { if (statusLifecycle.status == RoomStatus.Attached) { return @@ -269,15 +253,20 @@ internal class DefaultRoom( if (it.current == RoomStatus.Attached) { attachDeferred.complete(Unit) } else { - attachDeferred.completeExceptionally(roomInvalidStateException(statusLifecycle.status)) + val invalidStateException = + roomInvalidStateException(statusLifecycle.status, HttpStatusCode.InternalServerError) + attachDeferred.completeExceptionally(invalidStateException) } } - else -> attachDeferred.completeExceptionally(roomInvalidStateException(statusLifecycle.status)) + else -> { + val invalidStateException = roomInvalidStateException(statusLifecycle.status, HttpStatusCode.InternalServerError) + attachDeferred.completeExceptionally(invalidStateException) + } } } attachDeferred.await() return } - throw roomInvalidStateException(statusLifecycle.status) + throw roomInvalidStateException(statusLifecycle.status, HttpStatusCode.BadRequest) } } diff --git a/chat-android/src/main/java/com/ably/chat/RoomReactions.kt b/chat-android/src/main/java/com/ably/chat/RoomReactions.kt index b3385224..a5ce191b 100644 --- a/chat-android/src/main/java/com/ably/chat/RoomReactions.kt +++ b/chat-android/src/main/java/com/ably/chat/RoomReactions.kt @@ -3,7 +3,6 @@ package com.ably.chat import com.google.gson.JsonObject -import io.ably.lib.realtime.AblyRealtime import io.ably.lib.types.AblyException import io.ably.lib.types.ErrorInfo import io.ably.lib.types.MessageExtras @@ -104,22 +103,21 @@ data class SendReactionParams( ) internal class DefaultRoomReactions( - roomId: String, - private val clientId: String, - private val realtimeChannels: AblyRealtime.Channels, - private val logger: Logger, -) : RoomReactions, ContributesToRoomLifecycleImpl(logger) { + private val room: DefaultRoom, +) : RoomReactions, ContributesToRoomLifecycleImpl(room.roomLogger) { override val featureName = "reactions" - private val roomReactionsChannelName = "$roomId::\$chat::\$reactions" + private val roomReactionsChannelName = "${room.roomId}::\$chat::\$reactions" - override val channel: AblyRealtimeChannel = realtimeChannels.get(roomReactionsChannelName, ChatChannelOptions()) + override val channel: AblyRealtimeChannel = room.realtimeClient.channels.get(roomReactionsChannelName, ChatChannelOptions()) override val attachmentErrorCode: ErrorCode = ErrorCode.ReactionsAttachmentFailed override val detachmentErrorCode: ErrorCode = ErrorCode.ReactionsDetachmentFailed + private val logger = room.roomLogger.withContext(tag = "Reactions") + // (CHA-ER3) Ephemeral room reactions are sent to Ably via the Realtime connection via a send method. // (CHA-ER3a) Reactions are sent on the channel using a message in a particular format - see spec for format. override suspend fun send(params: SendReactionParams) { @@ -137,6 +135,7 @@ internal class DefaultRoomReactions( ) } } + room.ensureAttached() // TODO - This check might be removed in the future due to core spec change channel.publishCoroutine(pubSubMessage) } @@ -154,7 +153,7 @@ internal class DefaultRoomReactions( clientId = pubSubMessage.clientId, metadata = data.get("metadata")?.toMap() ?: mapOf(), headers = pubSubMessage.extras?.asJsonObject()?.get("headers")?.toMap() ?: mapOf(), - isSelf = pubSubMessage.clientId == clientId, + isSelf = pubSubMessage.clientId == room.clientId, ) listener.onReaction(reaction) } @@ -163,6 +162,6 @@ internal class DefaultRoomReactions( } override fun release() { - realtimeChannels.release(channel.name) + room.realtimeClient.channels.release(channel.name) } } diff --git a/chat-android/src/main/java/com/ably/chat/Typing.kt b/chat-android/src/main/java/com/ably/chat/Typing.kt index 9b442bd6..59dbdf36 100644 --- a/chat-android/src/main/java/com/ably/chat/Typing.kt +++ b/chat-android/src/main/java/com/ably/chat/Typing.kt @@ -91,13 +91,9 @@ interface Typing : EmitsDiscontinuities { data class TypingEvent(val currentlyTyping: Set) internal class DefaultTyping( - roomId: String, - private val realtimeClient: RealtimeClient, - private val clientId: String, - private val options: TypingOptions?, - private val logger: Logger, -) : Typing, ContributesToRoomLifecycleImpl(logger) { - private val typingIndicatorsChannelName = "$roomId::\$chat::\$typingIndicators" + private val room: DefaultRoom, +) : Typing, ContributesToRoomLifecycleImpl(room.roomLogger) { + private val typingIndicatorsChannelName = "${room.roomId}::\$chat::\$typingIndicators" override val featureName = "typing" @@ -105,6 +101,8 @@ internal class DefaultTyping( override val detachmentErrorCode: ErrorCode = ErrorCode.TypingDetachmentFailed + private val logger = room.roomLogger.withContext(tag = "Typing") + private val typingScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1) + SupervisorJob()) private val eventBus = MutableSharedFlow( @@ -112,7 +110,7 @@ internal class DefaultTyping( onBufferOverflow = BufferOverflow.DROP_OLDEST, ) - override val channel: Channel = realtimeClient.channels.get(typingIndicatorsChannelName, ChatChannelOptions()) + override val channel: Channel = room.realtimeClient.channels.get(typingIndicatorsChannelName, ChatChannelOptions()) private var typingJob: Job? = null @@ -155,6 +153,7 @@ internal class DefaultTyping( override suspend fun get(): Set { logger.trace("DefaultTyping.get()") + room.ensureAttached() return channel.presence.getCoroutine().map { it.clientId }.toSet() } @@ -169,7 +168,8 @@ internal class DefaultTyping( startTypingTimer() } else { startTypingTimer() - channel.presence.enterClientCoroutine(clientId) + room.ensureAttached() + channel.presence.enterClientCoroutine(room.clientId) } }.join() } @@ -178,18 +178,19 @@ internal class DefaultTyping( logger.trace("DefaultTyping.stop()") typingScope.launch { typingJob?.cancel() - channel.presence.leaveClientCoroutine(clientId) + room.ensureAttached() + channel.presence.leaveClientCoroutine(room.clientId) }.join() } override fun release() { presenceSubscription.unsubscribe() typingScope.cancel() - realtimeClient.channels.release(channel.name) + room.realtimeClient.channels.release(channel.name) } private fun startTypingTimer() { - val timeout = options?.timeoutMs ?: throw AblyException.fromErrorInfo( + val timeout = room.options.typing?.timeoutMs ?: throw AblyException.fromErrorInfo( ErrorInfo( "Typing options hasn't been initialized", ErrorCode.BadRequest.code, diff --git a/chat-android/src/main/java/com/ably/chat/Utils.kt b/chat-android/src/main/java/com/ably/chat/Utils.kt index 11ccb699..7e383a16 100644 --- a/chat-android/src/main/java/com/ably/chat/Utils.kt +++ b/chat-android/src/main/java/com/ably/chat/Utils.kt @@ -221,8 +221,8 @@ fun lifeCycleException( cause: Throwable? = null, ): AblyException = createAblyException(errorInfo, cause) -fun roomInvalidStateException(roomStatus: RoomStatus) = - ablyException("Can't perform operation, room is in an invalid state: $roomStatus", ErrorCode.RoomInInvalidState) +fun roomInvalidStateException(roomStatus: RoomStatus, statusCode: Int) = + ablyException("Can't perform operation, room is in an invalid state: $roomStatus", ErrorCode.RoomInInvalidState, statusCode) fun ablyException( errorMessage: String, diff --git a/chat-android/src/test/java/com/ably/chat/OccupancyTest.kt b/chat-android/src/test/java/com/ably/chat/OccupancyTest.kt index bb64cb2c..39a67991 100644 --- a/chat-android/src/test/java/com/ably/chat/OccupancyTest.kt +++ b/chat-android/src/test/java/com/ably/chat/OccupancyTest.kt @@ -1,13 +1,12 @@ package com.ably.chat +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 com.google.gson.JsonObject -import io.ably.lib.realtime.AblyRealtime.Channels -import io.ably.lib.realtime.Channel -import io.ably.lib.realtime.buildRealtimeChannel import io.mockk.every -import io.mockk.mockk import io.mockk.slot -import io.mockk.spyk import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -16,24 +15,20 @@ import org.junit.Test class OccupancyTest { - private val realtimeClient = mockk(relaxed = true) - private val realtimeChannels = mockk(relaxed = true) - private val realtimeChannel = spyk(buildRealtimeChannel()) - private val chatApi = spyk(ChatApi(realtimeClient, "clientId", EmptyLogger(LogContext(tag = "TEST")))) private lateinit var occupancy: Occupancy private val pubSubMessageListenerSlot = slot() + private val realtimeClient = createMockRealtimeClient() @Before fun setUp() { - every { realtimeChannels.get(any(), any()) } returns realtimeChannel - every { realtimeChannel.subscribe(capture(pubSubMessageListenerSlot)) } returns Unit + val mockRealtimeChannel = realtimeClient.createMockChannel() + every { mockRealtimeChannel.subscribe(capture(pubSubMessageListenerSlot)) } returns Unit - occupancy = DefaultOccupancy( - roomId = "room1", - realtimeChannels = realtimeChannels, - chatApi = chatApi, - logger = EmptyLogger(LogContext(tag = "TEST")), - ) + every { realtimeClient.channels.get(any(), any()) } returns mockRealtimeChannel + + val mockChatApi = createMockChatApi(realtimeClient) + val room = createMockRoom("room1", realtimeClient = realtimeClient, chatApi = mockChatApi) + occupancy = DefaultOccupancy(room) } /** diff --git a/chat-android/src/test/java/com/ably/chat/PresenceTest.kt b/chat-android/src/test/java/com/ably/chat/PresenceTest.kt index 3a302edf..718bbe22 100644 --- a/chat-android/src/test/java/com/ably/chat/PresenceTest.kt +++ b/chat-android/src/test/java/com/ably/chat/PresenceTest.kt @@ -1,16 +1,16 @@ package com.ably.chat -import com.ably.chat.room.createMockLogger +import com.ably.chat.room.createMockChannel +import com.ably.chat.room.createMockRealtimeClient +import com.ably.chat.room.createMockRoom import com.google.gson.JsonObject import com.google.gson.JsonPrimitive -import io.ably.lib.realtime.Channel import io.ably.lib.realtime.Presence.PresenceListener -import io.ably.lib.realtime.buildRealtimeChannel +import io.ably.lib.types.ChannelOptions import io.ably.lib.types.PresenceMessage import io.mockk.every import io.mockk.mockk import io.mockk.slot -import io.mockk.spyk import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before @@ -18,19 +18,19 @@ import org.junit.Test import io.ably.lib.realtime.Presence as PubSubPresence class PresenceTest { - private val pubSubChannel = spyk(buildRealtimeChannel("room1::\$chat::\$messages")) + private val pubSubPresence = mockk(relaxed = true) private lateinit var presence: DefaultPresence - private val logger = createMockLogger() @Before fun setUp() { - presence = DefaultPresence( - clientId = "client1", - channel = pubSubChannel, - presence = pubSubPresence, - logger, - ) + val realtimeClient = createMockRealtimeClient() + val mockRealtimeChannel = realtimeClient.createMockChannel("room1::\$chat::\$messages") + mockRealtimeChannel.setPrivateField("presence", pubSubPresence) + + every { realtimeClient.channels.get(any(), any()) } returns mockRealtimeChannel + + presence = DefaultPresence(createMockRoom(realtimeClient = realtimeClient)) } /** diff --git a/chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt b/chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt index 2d6bf327..d8745d58 100644 --- a/chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt +++ b/chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt @@ -1,15 +1,14 @@ package com.ably.chat -import com.ably.chat.room.createMockLogger +import com.ably.chat.room.createMockChannel +import com.ably.chat.room.createMockRealtimeClient +import com.ably.chat.room.createMockRoom import com.google.gson.JsonObject -import io.ably.lib.realtime.AblyRealtime.Channels import io.ably.lib.realtime.Channel import io.ably.lib.realtime.buildRealtimeChannel import io.ably.lib.types.MessageExtras import io.mockk.every -import io.mockk.mockk import io.mockk.slot -import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -17,14 +16,16 @@ import org.junit.Before import org.junit.Test class RoomReactionsTest { - private val realtimeChannels = mockk(relaxed = true) - private val realtimeChannel = spyk(buildRealtimeChannel("room1::\$chat::\$reactions")) + private lateinit var realtimeChannel: Channel private lateinit var roomReactions: DefaultRoomReactions - private val logger = createMockLogger() + private lateinit var room: DefaultRoom @Before fun setUp() { - every { realtimeChannels.get(any(), any()) } answers { + val realtimeClient = createMockRealtimeClient() + realtimeChannel = realtimeClient.createMockChannel("room1::\$chat::\$reactions") + + every { realtimeClient.channels.get(any(), any()) } answers { val channelName = firstArg() if (channelName == "room1::\$chat::\$reactions") { realtimeChannel @@ -32,13 +33,8 @@ class RoomReactionsTest { buildRealtimeChannel(channelName) } } - - roomReactions = DefaultRoomReactions( - roomId = "room1", - clientId = "client1", - realtimeChannels = realtimeChannels, - logger, - ) + room = createMockRoom("room1", "client1", realtimeClient = realtimeClient) + roomReactions = DefaultRoomReactions(room) } /** @@ -46,15 +42,10 @@ class RoomReactionsTest { */ @Test fun `channel name is set according to the spec`() = runTest { - val roomReactions = DefaultRoomReactions( - roomId = "foo", - clientId = "client1", - realtimeChannels = realtimeChannels, - logger, - ) + val roomReactions = DefaultRoomReactions(room) assertEquals( - "foo::\$chat::\$reactions", + "room1::\$chat::\$reactions", roomReactions.channel.name, ) } diff --git a/chat-android/src/test/java/com/ably/chat/Sandbox.kt b/chat-android/src/test/java/com/ably/chat/Sandbox.kt index fd74e3bd..2a65d68e 100644 --- a/chat-android/src/test/java/com/ably/chat/Sandbox.kt +++ b/chat-android/src/test/java/com/ably/chat/Sandbox.kt @@ -7,6 +7,8 @@ import io.ably.lib.realtime.ConnectionEvent import io.ably.lib.realtime.ConnectionState import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO +import io.ktor.client.network.sockets.ConnectTimeoutException +import io.ktor.client.network.sockets.SocketTimeoutException import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpRequestTimeoutException import io.ktor.client.request.get @@ -26,7 +28,9 @@ private val client = HttpClient(CIO) { !response.status.isSuccess() } retryOnExceptionIf { _, cause -> - cause is HttpRequestTimeoutException + cause is ConnectTimeoutException || + cause is HttpRequestTimeoutException || + cause is SocketTimeoutException } exponentialDelay() } diff --git a/chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt b/chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt index 7dd59af8..823c6445 100644 --- a/chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt +++ b/chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt @@ -4,6 +4,7 @@ import com.ably.chat.ChatApi import com.ably.chat.DefaultRoom import com.ably.chat.DefaultRoomLifecycle import com.ably.chat.ErrorCode +import com.ably.chat.HttpStatusCode import com.ably.chat.RoomLifecycle import com.ably.chat.RoomOptions import com.ably.chat.RoomStatus @@ -72,6 +73,7 @@ class RoomEnsureAttachedTest { Assert.assertTrue(result.isFailure) val exception = result.exceptionOrNull() as AblyException Assert.assertEquals(ErrorCode.RoomInInvalidState.code, exception.errorInfo.code) + Assert.assertEquals(HttpStatusCode.BadRequest, exception.errorInfo.statusCode) val errMsg = "Can't perform operation, room is in an invalid state: $invalidStatus" Assert.assertEquals(errMsg, exception.errorInfo.message) } @@ -160,6 +162,7 @@ class RoomEnsureAttachedTest { Assert.assertTrue(result.isFailure) val exception = result.exceptionOrNull() as AblyException Assert.assertEquals(ErrorCode.RoomInInvalidState.code, exception.errorInfo.code) + Assert.assertEquals(HttpStatusCode.InternalServerError, exception.errorInfo.statusCode) val errMsg = "Can't perform operation, room is in an invalid state: $invalidStatus" Assert.assertEquals(errMsg, exception.errorInfo.message) diff --git a/chat-android/src/test/java/com/ably/chat/room/RoomTestHelpers.kt b/chat-android/src/test/java/com/ably/chat/room/RoomTestHelpers.kt index ab24c9bc..cebff0b7 100644 --- a/chat-android/src/test/java/com/ably/chat/room/RoomTestHelpers.kt +++ b/chat-android/src/test/java/com/ably/chat/room/RoomTestHelpers.kt @@ -7,11 +7,13 @@ import com.ably.chat.ContributesToRoomLifecycle import com.ably.chat.DefaultMessages import com.ably.chat.DefaultOccupancy import com.ably.chat.DefaultPresence +import com.ably.chat.DefaultRoom import com.ably.chat.DefaultRoomLifecycle import com.ably.chat.DefaultRoomReactions import com.ably.chat.DefaultTyping import com.ably.chat.LifecycleOperationPrecedence import com.ably.chat.Logger +import com.ably.chat.RealtimeClient import com.ably.chat.Room import com.ably.chat.RoomLifecycleManager import com.ably.chat.RoomOptions @@ -19,7 +21,9 @@ import com.ably.chat.RoomStatusEventEmitter import com.ably.chat.Rooms import com.ably.chat.getPrivateField import com.ably.chat.invokePrivateSuspendMethod +import com.ably.chat.setPrivateField import io.ably.lib.realtime.AblyRealtime +import io.ably.lib.realtime.Channel import io.ably.lib.realtime.ChannelState import io.ably.lib.types.ClientOptions import io.ably.lib.types.ErrorInfo @@ -29,9 +33,37 @@ import io.mockk.spyk import kotlinx.coroutines.CompletableDeferred import io.ably.lib.realtime.Channel as AblyRealtimeChannel -fun createMockRealtimeClient(): AblyRealtime = spyk(AblyRealtime(ClientOptions("id:key").apply { autoConnect = false })) +const val DEFAULT_ROOM_ID = "1234" +const val DEFAULT_CLIENT_ID = "clientId" +const val DEFAULT_CHANNEL_NAME = "channel" + +fun createMockRealtimeClient(): AblyRealtime { + val realtimeClient = spyk(AblyRealtime(ClientOptions("id:key").apply { autoConnect = false }), recordPrivateCalls = true) + val mockChannels = spyk(realtimeClient.channels, recordPrivateCalls = true) + realtimeClient.setPrivateField("channels", mockChannels) + return realtimeClient +} + +fun AblyRealtime.createMockChannel(channelName: String = DEFAULT_CHANNEL_NAME): Channel = + spyk(channels.get(channelName), recordPrivateCalls = true) + +internal fun createMockChatApi( + realtimeClient: RealtimeClient = createMockRealtimeClient(), + clientId: String = DEFAULT_CLIENT_ID, + logger: Logger = createMockLogger(), +) = spyk(ChatApi(realtimeClient, clientId, logger), recordPrivateCalls = true) + internal fun createMockLogger(): Logger = mockk(relaxed = true) +internal fun createMockRoom( + roomId: String = DEFAULT_ROOM_ID, + clientId: String = DEFAULT_CLIENT_ID, + realtimeClient: RealtimeClient = createMockRealtimeClient(), + chatApi: ChatApi = mockk(relaxed = true), + logger: Logger = createMockLogger(), +): DefaultRoom = + DefaultRoom(roomId, RoomOptions.default, realtimeClient, chatApi, clientId, logger) + // Rooms mocks val Rooms.RoomIdToRoom get() = getPrivateField>("roomIdToRoom") val Rooms.RoomGetDeferred get() = getPrivateField>>("roomGetDeferred") @@ -60,24 +92,17 @@ internal suspend fun RoomLifecycleManager.atomicRetry(exceptContributor: Contrib }.await() } -fun createRoomFeatureMocks(roomId: String = "1234"): List { - val clientId = "clientId" - +fun createRoomFeatureMocks(roomId: String = DEFAULT_ROOM_ID, clientId: String = DEFAULT_CLIENT_ID): List { val realtimeClient = createMockRealtimeClient() - val chatApi = mockk(relaxed = true) + val chatApi = createMockChatApi() val logger = createMockLogger() + val room = createMockRoom(roomId, clientId, realtimeClient, chatApi, logger) val messagesContributor = spyk(DefaultMessages(roomId, realtimeClient.channels, chatApi, logger), recordPrivateCalls = true) - val presenceContributor = spyk( - DefaultPresence(clientId, messagesContributor.channel, messagesContributor.channel.presence, logger), - recordPrivateCalls = true, - ) - val occupancyContributor = spyk(DefaultOccupancy(realtimeClient.channels, chatApi, roomId, logger), recordPrivateCalls = true) - val typingContributor = spyk( - DefaultTyping(roomId, realtimeClient, clientId, RoomOptions.default.typing, logger), - recordPrivateCalls = true, - ) - val reactionsContributor = spyk(DefaultRoomReactions(roomId, clientId, realtimeClient.channels, logger), recordPrivateCalls = true) + val presenceContributor = spyk(DefaultPresence(room), recordPrivateCalls = true) + val occupancyContributor = spyk(DefaultOccupancy(room), recordPrivateCalls = true) + val typingContributor = spyk(DefaultTyping(room), recordPrivateCalls = true) + val reactionsContributor = spyk(DefaultRoomReactions(room), recordPrivateCalls = true) return listOf(messagesContributor, presenceContributor, occupancyContributor, typingContributor, reactionsContributor) }