diff --git a/Tests/AblyChatTests/DefaultRoomTypingTests.swift b/Tests/AblyChatTests/DefaultRoomTypingTests.swift new file mode 100644 index 0000000..5641496 --- /dev/null +++ b/Tests/AblyChatTests/DefaultRoomTypingTests.swift @@ -0,0 +1,199 @@ +import Ably +@testable import AblyChat +import Testing + +struct DefaultRoomTypingTests { + // MARK: - Test helpers + + // @spec CHA-T2 + @Test + func retrieveCurrentlyTypingClientIDs() async throws { + // Given + let typingPresence = MockRealtimePresence(["client1", "client2"].map { .init(clientId: $0) }) + let channel = MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", mockPresence: typingPresence) + let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(())) + let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger(), timeout: 5) + + // When + let typingInfo = try await defaultTyping.get() + + // Then + #expect(typingInfo.sorted() == ["client1", "client2"]) + } + + // @specPartial CHA-T2c + @Test + func retrieveCurrentlyTypingClientIDsWhileAttaching() async throws { + // Given: A DefaultRoomLifecycleManager, with an ATTACH operation in progress and hence in the ATTACHING status + let contributor = RoomLifecycleHelper.createContributor(feature: .presence, attachBehavior: .completeAndChangeState(.success, newState: .attached, delayInMilliseconds: RoomLifecycleHelper.fakeNetworkDelay)) + let lifecycleManager = await RoomLifecycleHelper.createManager(contributors: [contributor]) + + // Given: A DefaultPresence with DefaultFeatureChannel and MockRoomLifecycleContributor + let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) }) + let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence) + let featureChannel = DefaultFeatureChannel(channel: channel, contributor: contributor, roomLifecycleManager: lifecycleManager) + let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger(), timeout: 5) + + let attachingStatusWaitSubscription = await lifecycleManager.testsOnly_subscribeToStatusChangeWaitEvents() + + // When: The room is in the attaching state + let roomStatusSubscription = await lifecycleManager.onRoomStatusChange(bufferingPolicy: .unbounded) + + let attachOperationID = UUID() + async let _ = lifecycleManager.performAttachOperation(testsOnly_forcingOperationID: attachOperationID) + + // Wait for room to become ATTACHING + _ = try #require(await roomStatusSubscription.attachingElements().first { _ in true }) + + // When: And presence get is called + _ = try await defaultTyping.get() + + // Then: The manager was waiting for its room status to change before presence `get` was called + _ = try #require(await attachingStatusWaitSubscription.first { _ in true }) + } + + // @specPartial CHA-T2c + @Test + func retrieveCurrentlyTypingClientIDsWhileAttachingWithFailure() async throws { + // Given: attachment failure + let attachError = ARTErrorInfo(domain: "SomeDomain", code: 123) + + // Given: A DefaultRoomLifecycleManager, with an ATTACH operation in progress and hence in the ATTACHING status + let contributor = RoomLifecycleHelper.createContributor(feature: .presence, attachBehavior: .completeAndChangeState(.failure(attachError), newState: .failed, delayInMilliseconds: RoomLifecycleHelper.fakeNetworkDelay)) + let lifecycleManager = await RoomLifecycleHelper.createManager(contributors: [contributor]) + + // Given: A DefaultPresence with DefaultFeatureChannel and MockRoomLifecycleContributor + let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) }) + let channel = MockRealtimeChannel(mockPresence: realtimePresence) + let featureChannel = DefaultFeatureChannel(channel: channel, contributor: contributor, roomLifecycleManager: lifecycleManager) + let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger(), timeout: 5) + + let attachingStatusWaitSubscription = await lifecycleManager.testsOnly_subscribeToStatusChangeWaitEvents() + + // When: The room is in the attaching state + let roomStatusSubscription = await lifecycleManager.onRoomStatusChange(bufferingPolicy: .unbounded) + + let attachOperationID = UUID() + async let _ = lifecycleManager.performAttachOperation(testsOnly_forcingOperationID: attachOperationID) + + // Wait for room to become ATTACHING + _ = try #require(await roomStatusSubscription.attachingElements().first { _ in true }) + + // When: And fails to attach + await #expect(throws: ARTErrorInfo.self) { + do { + _ = try await defaultTyping.get() + } catch { + // Then: An exception with status code of 500 should be thrown + let error = try #require(error as? ARTErrorInfo) + #expect(error.statusCode == 500) + #expect(error.code == ErrorCode.roomInInvalidState.rawValue) + throw error + } + } + // Then: The manager were waiting for its room status to change from attaching + _ = try #require(await attachingStatusWaitSubscription.first { _ in true }) + } + + // @spec CHA-T2g + @Test + func failToRetrieveCurrentlyTypingClientIDsWhenRoomInInvalidState() async throws { + // Given + let realtimePresence = MockRealtimePresence(["client1", "client2"].map { .init(clientId: $0) }) + let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence) + let error = ARTErrorInfo(chatError: .presenceOperationRequiresRoomAttach(feature: .presence)) + let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .failure(error)) + let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger(), timeout: 5) + + // Then + await #expect(throws: ARTErrorInfo.self) { + do { + _ = try await defaultTyping.get() + } catch { + let error = try #require(error as? ARTErrorInfo) + #expect(error.statusCode == 400) + #expect(error.localizedDescription.contains("attach")) + throw error + } + } + } + + // @spec CHA-T4 + // @spec CHA-T5 + @Test + func usersMayIndicateThatTheyHaveStartedOrStoppedTyping() async throws { + // Given + let typingPresence = MockRealtimePresence([]) + let channel = MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", mockPresence: typingPresence) + let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(())) + let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger(), timeout: 5) + + // CHA-T4 + + // When + try await defaultTyping.start() + + // Then + var typingInfo = try await defaultTyping.get() + #expect(typingInfo == ["client1"]) + + // CHA-T5 + + // When + try await defaultTyping.stop() + + // Then + typingInfo = try await defaultTyping.get() + #expect(typingInfo.isEmpty) + } + + // @spec CHA-T6a + // @spec CHA-T6b + @Test + func usersMaySubscribeToTypingEvents() async throws { + // Given + let typingPresence = MockRealtimePresence([]) + let channel = MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", mockPresence: typingPresence) + let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(())) + let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger(), timeout: 5) + + // CHA-T6a + + // When + let subscription = await defaultTyping.subscribe() + subscription.emit(TypingEvent(currentlyTyping: ["client1"])) + + // Then + let typingEvent = try #require(await subscription.first { _ in true }) + #expect(typingEvent.currentlyTyping == ["client1"]) + + // CHA-T6b + + // When + subscription.unsubscribe() + subscription.emit(TypingEvent(currentlyTyping: ["client1"])) + + // Then + let nilTypingEvento = await subscription.first { _ in true } + #expect(nilTypingEvento == nil) + } + + // @spec CHA-T7 + @Test + func onDiscontinuity() async throws { + // Given + let typingPresence = MockRealtimePresence([]) + let channel = MockRealtimeChannel(mockPresence: typingPresence) + let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(())) + let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger(), timeout: 5) + + // When: The feature channel emits a discontinuity through `onDiscontinuity` + let featureChannelDiscontinuity = DiscontinuityEvent(error: ARTErrorInfo.createUnknownError()) // arbitrary error + let discontinuitySubscription = await defaultTyping.onDiscontinuity() + await featureChannel.emitDiscontinuity(featureChannelDiscontinuity) + + // Then: The DefaultOccupancy instance emits this discontinuity through `onDiscontinuity` + let discontinuity = try #require(await discontinuitySubscription.first { _ in true }) + #expect(discontinuity == featureChannelDiscontinuity) + } +}