Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 105 additions & 44 deletions Tests/AblyChatTests/DefaultPresenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Ably
@testable import AblyChat
import Testing

@MainActor
struct DefaultPresenceTests {
// MARK: CHA-PR3

Expand All @@ -10,9 +11,9 @@ struct DefaultPresenceTests {
@Test
func usersMayEnterPresence() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let defaultPresence = await DefaultPresence(
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: MockRoomLifecycleManager(),
roomName: "basketball",
Expand All @@ -34,9 +35,9 @@ struct DefaultPresenceTests {
@Test
func usersMayEnterPresenceWithoutData() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let defaultPresence = await DefaultPresence(
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: MockRoomLifecycleManager(),
roomName: "basketball",
Expand All @@ -58,10 +59,10 @@ struct DefaultPresenceTests {
@Test
func usersMayEnterPresenceWhileAttaching() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager()
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager()
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -86,10 +87,10 @@ struct DefaultPresenceTests {
let attachError = ErrorInfo.createArbitraryError()
let error = InternalError.roomTransitionedToInvalidStateForPresenceOperation(newState: .failed /* arbitrary */, cause: attachError).toErrorInfo()

let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -116,10 +117,10 @@ struct DefaultPresenceTests {
func failToEnterPresenceWhenRoomInInvalidState() async throws {
// Given
let error = InternalError.presenceOperationRequiresRoomAttach.toErrorInfo()
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -141,10 +142,10 @@ struct DefaultPresenceTests {
@Test
func usersMayUpdatePresence() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager()
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager()
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -165,10 +166,10 @@ struct DefaultPresenceTests {
@Test
func usersMayUpdatePresenceWhileAttaching() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager()
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager()
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -193,10 +194,10 @@ struct DefaultPresenceTests {
let attachError = ErrorInfo.createArbitraryError()
let error = InternalError.roomTransitionedToInvalidStateForPresenceOperation(newState: .failed /* arbitrary */, cause: attachError).toErrorInfo()

let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -223,10 +224,10 @@ struct DefaultPresenceTests {
func failToUpdatePresenceWhenRoomInInvalidState() async throws {
// Given
let error = InternalError.presenceOperationRequiresRoomAttach.toErrorInfo()
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -247,10 +248,10 @@ struct DefaultPresenceTests {
@Test
func usersMayLeavePresence() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager()
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager()
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -274,10 +275,10 @@ struct DefaultPresenceTests {
@Test
func ifUserIsPresent() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager()
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager()
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -302,10 +303,10 @@ struct DefaultPresenceTests {
@Test
func retrieveAllTheMembersOfThePresenceSet() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager()
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager()
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -328,10 +329,10 @@ struct DefaultPresenceTests {
func failToRetrieveAllTheMembersOfThePresenceSetWhenRoomInInvalidState() async throws {
// Given
let error = InternalError.presenceOperationRequiresRoomAttach.toErrorInfo()
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -350,10 +351,10 @@ struct DefaultPresenceTests {
@Test
func retrieveAllTheMembersOfThePresenceSetWhileAttaching() async throws {
// Given
let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager()
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager()
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -378,10 +379,10 @@ struct DefaultPresenceTests {
let attachError = ErrorInfo.createArbitraryError()
let error = InternalError.roomTransitionedToInvalidStateForPresenceOperation(newState: .failed /* arbitrary */, cause: attachError).toErrorInfo()

let channel = await MockRealtimeChannel(name: "basketball::$chat")
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let roomLifecycleManager = await MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = await DefaultPresence(
let roomLifecycleManager = MockRoomLifecycleManager(resultOfWaitToBeAbleToPerformPresenceOperations: .failure(error))
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: roomLifecycleManager,
roomName: "basketball",
Expand All @@ -405,5 +406,65 @@ struct DefaultPresenceTests {

// MARK: CHA-PR7

// TODO: Test (https://github.com/ably/ably-chat-swift/issues/396)
// @spec CHA-PR7a
// @spec CHA-PR7c
// @specUntested CHA-PR7d - We chose to implement this failure with an idiomatic fatalError instead of throwing, but we can't test this.
@Test
func usersMaySubscribeToAllPresenceEvents() async throws {
// Given: A channel that will emit presence messages
let channel = MockRealtimeChannel(name: "basketball::$chat")
let logger = TestLogger()
let defaultPresence = DefaultPresence(
channel: channel,
roomLifecycleManager: MockRoomLifecycleManager(),
roomName: "basketball",
logger: logger,
options: .init(enableEvents: true),
)

// Track received events
var receivedEvents: [PresenceEvent] = []

// When: Subscribe to presence events
let subscription = defaultPresence.subscribe { event in
receivedEvents.append(event)
}

// Simulate receiving presence messages from the channel
let enterMessage = ARTPresenceMessage()
enterMessage.action = .enter
enterMessage.clientId = "client1"
enterMessage.data = ["status": "online"]
enterMessage.timestamp = Date()

let updateMessage = ARTPresenceMessage()
updateMessage.action = .update
updateMessage.clientId = "client1"
updateMessage.data = ["status": "busy"]
updateMessage.timestamp = Date()

let leaveMessage = ARTPresenceMessage()
leaveMessage.action = .leave
leaveMessage.clientId = "client1"
leaveMessage.timestamp = Date()

channel.emitPresenceMessage(enterMessage)
channel.emitPresenceMessage(updateMessage)
channel.emitPresenceMessage(leaveMessage)

// Then: All events are received
#expect(receivedEvents.count == 3)
#expect(receivedEvents[0].type == .enter)
#expect(receivedEvents[1].type == .update)
#expect(receivedEvents[2].type == .leave)

// Clean up
receivedEvents.removeAll()
subscription.unsubscribe()

channel.emitPresenceMessage(enterMessage)

// Then: No events are received after unsubscribing
#expect(receivedEvents.isEmpty)
}
}
7 changes: 6 additions & 1 deletion Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Ably
@testable import AblyChat

final class MockRealtimeChannel: InternalRealtimeChannelProtocol {
let presence = MockRealtimePresence()
let presence: MockRealtimePresence
let annotations: MockRealtimeAnnotations
let proxied = MockAblyCocoaRealtime.Channel()

Expand Down Expand Up @@ -44,6 +44,7 @@ final class MockRealtimeChannel: InternalRealtimeChannelProtocol {
attachSerial = properties.attachSerial
channelSerial = properties.channelSerial
self.stateChangeToEmitForListener = stateChangeToEmitForListener
presence = MockRealtimePresence()
annotations = MockRealtimeAnnotations(annotationToEmitOnSubscribe: annotationToEmitOnSubscribe)
}

Expand Down Expand Up @@ -208,4 +209,8 @@ final class MockRealtimeChannel: InternalRealtimeChannelProtocol {
func publish(_ name: String?, data: JSONValue?, extras: [String: JSONValue]?) {
publishedMessages.append(TestMessage(name: name, data: data, extras: extras))
}

func emitPresenceMessage(_ message: ARTPresenceMessage) {
presence.emitMessage(message)
}
}
20 changes: 15 additions & 5 deletions Tests/AblyChatTests/Mocks/MockRealtimePresence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@ import Ably

final class MockRealtimePresence: InternalRealtimePresenceProtocol {
let callRecorder = MockMethodCallRecorder()
private var subscriptions: [@MainActor (ARTPresenceMessage) -> Void] = []

func subscribe(_: @escaping @MainActor (ARTPresenceMessage) -> Void) -> ARTEventListener? {
ARTEventListener()
func subscribe(_ callback: @escaping @MainActor (ARTPresenceMessage) -> Void) -> ARTEventListener? {
subscriptions.append(callback)
return ARTEventListener()
}

func subscribe(_: ARTPresenceAction, callback _: @escaping @MainActor (ARTPresenceMessage) -> Void) -> ARTEventListener? {
ARTEventListener()
func subscribe(_: ARTPresenceAction, callback: @escaping @MainActor (ARTPresenceMessage) -> Void) -> ARTEventListener? {
subscriptions.append(callback)
return ARTEventListener()
}

func unsubscribe(_: ARTEventListener) {
// no-op since it's called automatically
// Clear subscriptions when unsubscribing
subscriptions.removeAll()
}

func emitMessage(_ message: ARTPresenceMessage) {
for callback in subscriptions {
callback(message)
}
}

func get() async throws(ErrorInfo) -> [PresenceMessage] {
Expand Down