diff --git a/chat-android/src/main/java/com/ably/chat/ErrorCodes.kt b/chat-android/src/main/java/com/ably/chat/ErrorCodes.kt index a86dd8ef..2242babb 100644 --- a/chat-android/src/main/java/com/ably/chat/ErrorCodes.kt +++ b/chat-android/src/main/java/com/ably/chat/ErrorCodes.kt @@ -79,6 +79,11 @@ enum class ErrorCodes(val errorCode: Int) { */ RoomIsReleased(102_103), + /** + * Room was released before the operation could complete. + */ + RoomReleasedBeforeOperationCompleted(102_106), + /** * Cannot perform operation because the previous operation failed. */ 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 c2ce7f26..97418dab 100644 --- a/chat-android/src/main/java/com/ably/chat/Room.kt +++ b/chat-android/src/main/java/com/ably/chat/Room.kt @@ -2,7 +2,6 @@ package com.ably.chat -import io.ably.lib.types.AblyException import io.ably.lib.types.ErrorInfo import io.ably.lib.util.Log.LogHandler import kotlinx.coroutines.CoroutineName @@ -144,13 +143,7 @@ internal class DefaultRoom( override val presence: Presence get() { if (_presence == null) { - throw AblyException.fromErrorInfo( - ErrorInfo( - "Presence is not enabled for this room", - ErrorCodes.BadRequest.errorCode, - HttpStatusCodes.BadRequest, - ), - ) + throw ablyException("Presence is not enabled for this room", ErrorCodes.BadRequest) } return _presence as Presence } @@ -159,13 +152,7 @@ internal class DefaultRoom( override val reactions: RoomReactions get() { if (_reactions == null) { - throw AblyException.fromErrorInfo( - ErrorInfo( - "Reactions are not enabled for this room", - ErrorCodes.BadRequest.errorCode, - HttpStatusCodes.BadRequest, - ), - ) + throw ablyException("Reactions are not enabled for this room", ErrorCodes.BadRequest) } return _reactions as RoomReactions } @@ -174,13 +161,7 @@ internal class DefaultRoom( override val typing: Typing get() { if (_typing == null) { - throw AblyException.fromErrorInfo( - ErrorInfo( - "Typing is not enabled for this room", - ErrorCodes.BadRequest.errorCode, - HttpStatusCodes.BadRequest, - ), - ) + throw ablyException("Typing is not enabled for this room", ErrorCodes.BadRequest) } return _typing as Typing } @@ -189,13 +170,7 @@ internal class DefaultRoom( override val occupancy: Occupancy get() { if (_occupancy == null) { - throw AblyException.fromErrorInfo( - ErrorInfo( - "Occupancy is not enabled for this room", - ErrorCodes.BadRequest.errorCode, - HttpStatusCodes.BadRequest, - ), - ) + throw ablyException("Occupancy is not enabled for this room", ErrorCodes.BadRequest) } return _occupancy as Occupancy } diff --git a/chat-android/src/main/java/com/ably/chat/RoomOptions.kt b/chat-android/src/main/java/com/ably/chat/RoomOptions.kt index ba1efd09..cec8378a 100644 --- a/chat-android/src/main/java/com/ably/chat/RoomOptions.kt +++ b/chat-android/src/main/java/com/ably/chat/RoomOptions.kt @@ -1,8 +1,5 @@ package com.ably.chat -import io.ably.lib.types.AblyException -import io.ably.lib.types.ErrorInfo - /** * Represents the options for a given chat room. */ @@ -31,7 +28,19 @@ data class RoomOptions( * {@link RoomOptionsDefaults.occupancy} to enable occupancy with default options. */ val occupancy: OccupancyOptions? = null, -) +) { + companion object { + /** + * Supports all room options with default values + */ + val default = RoomOptions( + typing = TypingOptions(), + presence = PresenceOptions(), + reactions = RoomReactionsOptions, + occupancy = OccupancyOptions, + ) + } +} /** * Represents the presence options for a chat room. @@ -81,12 +90,6 @@ object OccupancyOptions */ fun RoomOptions.validateRoomOptions() { if (typing != null && typing.timeoutMs <= 0) { - throw AblyException.fromErrorInfo( - ErrorInfo( - "Typing timeout must be greater than 0", - ErrorCodes.InvalidRequestBody.errorCode, - HttpStatusCodes.BadRequest, - ), - ) + throw ablyException("Typing timeout must be greater than 0", ErrorCodes.InvalidRequestBody) } } 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 cc0e47ad..f1cdc013 100644 --- a/chat-android/src/main/java/com/ably/chat/Utils.kt +++ b/chat-android/src/main/java/com/ably/chat/Utils.kt @@ -195,3 +195,10 @@ internal class DeferredValue { return result } } + +fun ablyException( + errorMessage: String, + code: ErrorCodes, + statusCode: Int = HttpStatusCodes.BadRequest, + cause: Throwable? = null, +): AblyException = AblyException.fromErrorInfo(cause, ErrorInfo(errorMessage, statusCode, code.errorCode)) diff --git a/chat-android/src/test/java/com/ably/chat/room/ConfigureRoomOptionsTest.kt b/chat-android/src/test/java/com/ably/chat/room/ConfigureRoomOptionsTest.kt new file mode 100644 index 00000000..948945f9 --- /dev/null +++ b/chat-android/src/test/java/com/ably/chat/room/ConfigureRoomOptionsTest.kt @@ -0,0 +1,92 @@ +package com.ably.chat.room + +import com.ably.chat.ChatApi +import com.ably.chat.DefaultRoom +import com.ably.chat.RoomOptions +import com.ably.chat.RoomStatus +import com.ably.chat.TypingOptions +import com.ably.utils.createMockRealtimeClient +import io.ably.lib.types.AblyException +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Assert.assertThrows +import org.junit.Test + +/** + * Chat rooms are configurable, so as to enable or disable certain features. + * When requesting a room, options as to which features should be enabled, and + * the configuration they should take, must be provided + * Spec: CHA-RC2 + */ +class ConfigureRoomOptionsTest { + + @Test + fun `(CHA-RC2a) If a room is requested with a negative typing timeout, an ErrorInfo with code 40001 must be thrown`() = runTest { + val mockRealtimeClient = createMockRealtimeClient() + val chatApi = mockk(relaxed = true) + + // Room success when positive typing timeout + val room = DefaultRoom("1234", RoomOptions(typing = TypingOptions(timeoutMs = 100)), mockRealtimeClient, chatApi, null) + Assert.assertNotNull(room) + Assert.assertEquals(RoomStatus.Initialized, room.status) + + // Room failure when negative timeout + val exception = assertThrows(AblyException::class.java) { + DefaultRoom("1234", RoomOptions(typing = TypingOptions(timeoutMs = -1)), mockRealtimeClient, chatApi, null) + } + Assert.assertEquals("Typing timeout must be greater than 0", exception.errorInfo.message) + Assert.assertEquals(40_001, exception.errorInfo.code) + Assert.assertEquals(400, exception.errorInfo.statusCode) + } + + @Test + fun `(CHA-RC2b) Attempting to use disabled feature must result in an ErrorInfo with code 40000 being thrown`() = runTest { + val mockRealtimeClient = createMockRealtimeClient() + val chatApi = mockk(relaxed = true) + + // Room only supports messages feature, since by default other features are turned off + val room = DefaultRoom("1234", RoomOptions(), mockRealtimeClient, chatApi, null) + Assert.assertNotNull(room) + Assert.assertEquals(RoomStatus.Initialized, room.status) + + // Access presence throws exception + var exception = assertThrows(AblyException::class.java) { + room.presence + } + Assert.assertEquals("Presence is not enabled for this room", exception.errorInfo.message) + Assert.assertEquals(40_000, exception.errorInfo.code) + Assert.assertEquals(400, exception.errorInfo.statusCode) + + // Access reactions throws exception + exception = assertThrows(AblyException::class.java) { + room.reactions + } + Assert.assertEquals("Reactions are not enabled for this room", exception.errorInfo.message) + Assert.assertEquals(40_000, exception.errorInfo.code) + Assert.assertEquals(400, exception.errorInfo.statusCode) + + // Access typing throws exception + exception = assertThrows(AblyException::class.java) { + room.typing + } + Assert.assertEquals("Typing is not enabled for this room", exception.errorInfo.message) + Assert.assertEquals(40_000, exception.errorInfo.code) + Assert.assertEquals(400, exception.errorInfo.statusCode) + + // Access occupancy throws exception + exception = assertThrows(AblyException::class.java) { + room.occupancy + } + Assert.assertEquals("Occupancy is not enabled for this room", exception.errorInfo.message) + Assert.assertEquals(40_000, exception.errorInfo.code) + Assert.assertEquals(400, exception.errorInfo.statusCode) + + // room with all features + val roomWithAllFeatures = DefaultRoom("1234", RoomOptions.default, mockRealtimeClient, chatApi, null) + Assert.assertNotNull(roomWithAllFeatures.presence) + Assert.assertNotNull(roomWithAllFeatures.reactions) + Assert.assertNotNull(roomWithAllFeatures.typing) + Assert.assertNotNull(roomWithAllFeatures.occupancy) + } +} diff --git a/chat-android/src/test/java/com/ably/chat/room/RoomGetTest.kt b/chat-android/src/test/java/com/ably/chat/room/RoomGetTest.kt new file mode 100644 index 00000000..d9777ff9 --- /dev/null +++ b/chat-android/src/test/java/com/ably/chat/room/RoomGetTest.kt @@ -0,0 +1,3 @@ +package com.ably.chat.room + +class RoomGetTest diff --git a/chat-android/src/test/java/com/ably/chat/room/RoomReleaseTest.kt b/chat-android/src/test/java/com/ably/chat/room/RoomReleaseTest.kt new file mode 100644 index 00000000..a97a2223 --- /dev/null +++ b/chat-android/src/test/java/com/ably/chat/room/RoomReleaseTest.kt @@ -0,0 +1,3 @@ +package com.ably.chat.room + +class RoomReleaseTest diff --git a/chat-android/src/test/java/com/ably/chat/room/AttachTest.kt b/chat-android/src/test/java/com/ably/chat/room/lifecycle/AttachTest.kt similarity index 99% rename from chat-android/src/test/java/com/ably/chat/room/AttachTest.kt rename to chat-android/src/test/java/com/ably/chat/room/lifecycle/AttachTest.kt index 36b60f54..6a186896 100644 --- a/chat-android/src/test/java/com/ably/chat/room/AttachTest.kt +++ b/chat-android/src/test/java/com/ably/chat/room/lifecycle/AttachTest.kt @@ -1,4 +1,4 @@ -package com.ably.chat.room +package com.ably.chat.room.lifecycle import com.ably.chat.ContributesToRoomLifecycle import com.ably.chat.DefaultRoomLifecycle diff --git a/chat-android/src/test/java/com/ably/chat/room/DetachTest.kt b/chat-android/src/test/java/com/ably/chat/room/lifecycle/DetachTest.kt similarity index 99% rename from chat-android/src/test/java/com/ably/chat/room/DetachTest.kt rename to chat-android/src/test/java/com/ably/chat/room/lifecycle/DetachTest.kt index b1f5aa52..673986eb 100644 --- a/chat-android/src/test/java/com/ably/chat/room/DetachTest.kt +++ b/chat-android/src/test/java/com/ably/chat/room/lifecycle/DetachTest.kt @@ -1,4 +1,4 @@ -package com.ably.chat.room +package com.ably.chat.room.lifecycle import com.ably.chat.ContributesToRoomLifecycle import com.ably.chat.DefaultRoomLifecycle diff --git a/chat-android/src/test/java/com/ably/chat/room/PrecedenceTest.kt b/chat-android/src/test/java/com/ably/chat/room/lifecycle/PrecedenceTest.kt similarity index 99% rename from chat-android/src/test/java/com/ably/chat/room/PrecedenceTest.kt rename to chat-android/src/test/java/com/ably/chat/room/lifecycle/PrecedenceTest.kt index 19dfbe10..f7df70c2 100644 --- a/chat-android/src/test/java/com/ably/chat/room/PrecedenceTest.kt +++ b/chat-android/src/test/java/com/ably/chat/room/lifecycle/PrecedenceTest.kt @@ -1,4 +1,4 @@ -package com.ably.chat.room +package com.ably.chat.room.lifecycle import com.ably.chat.ContributesToRoomLifecycle import com.ably.chat.DefaultRoomAttachmentResult diff --git a/chat-android/src/test/java/com/ably/chat/room/ReleaseTest.kt b/chat-android/src/test/java/com/ably/chat/room/lifecycle/ReleaseTest.kt similarity index 99% rename from chat-android/src/test/java/com/ably/chat/room/ReleaseTest.kt rename to chat-android/src/test/java/com/ably/chat/room/lifecycle/ReleaseTest.kt index 051f45b9..b80ad727 100644 --- a/chat-android/src/test/java/com/ably/chat/room/ReleaseTest.kt +++ b/chat-android/src/test/java/com/ably/chat/room/lifecycle/ReleaseTest.kt @@ -1,4 +1,4 @@ -package com.ably.chat.room +package com.ably.chat.room.lifecycle import com.ably.chat.DefaultRoomLifecycle import com.ably.chat.RoomLifecycleManager diff --git a/chat-android/src/test/java/com/ably/chat/room/RetryTest.kt b/chat-android/src/test/java/com/ably/chat/room/lifecycle/RetryTest.kt similarity index 99% rename from chat-android/src/test/java/com/ably/chat/room/RetryTest.kt rename to chat-android/src/test/java/com/ably/chat/room/lifecycle/RetryTest.kt index b65f301a..fdc7b42f 100644 --- a/chat-android/src/test/java/com/ably/chat/room/RetryTest.kt +++ b/chat-android/src/test/java/com/ably/chat/room/lifecycle/RetryTest.kt @@ -1,4 +1,4 @@ -package com.ably.chat.room +package com.ably.chat.room.lifecycle import com.ably.chat.DefaultRoomLifecycle import com.ably.chat.HttpStatusCodes diff --git a/chat-android/src/test/java/com/ably/utils/RoomTestHelpers.kt b/chat-android/src/test/java/com/ably/utils/RoomTestHelpers.kt index 0c99ff4b..949d653a 100644 --- a/chat-android/src/test/java/com/ably/utils/RoomTestHelpers.kt +++ b/chat-android/src/test/java/com/ably/utils/RoomTestHelpers.kt @@ -36,8 +36,10 @@ fun AblyRealtimeChannel.setState(state: ChannelState, errorInfo: ErrorInfo? = nu this.reason = errorInfo } +fun createMockRealtimeClient(): AblyRealtime = spyk(AblyRealtime(ClientOptions("id:key").apply { autoConnect = false })) + fun createRoomFeatureMocks(roomId: String = "1234"): List { - val realtimeClient = spyk(AblyRealtime(ClientOptions("id:key").apply { autoConnect = false })) + val realtimeClient = createMockRealtimeClient() val chatApi = mockk(relaxed = true) val messagesContributor = spyk(DefaultMessages(roomId, realtimeClient.channels, chatApi), recordPrivateCalls = true)