Skip to content

Commit f1868c2

Browse files
committed
Added presence tests.
1 parent fb70b2e commit f1868c2

9 files changed

+702
-16
lines changed

Sources/AblyChat/Dependencies.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public protocol RealtimeChannelsProtocol: ARTRealtimeChannelsProtocol, Sendable
2525
/// Expresses the requirements of the object returned by ``RealtimeChannelsProtocol/get(_:options:)``.
2626
public protocol RealtimeChannelProtocol: ARTRealtimeChannelProtocol, Sendable {}
2727

28+
public protocol RealtimePresenceProtocol: ARTRealtimePresenceProtocol, Sendable {}
29+
2830
public protocol ConnectionProtocol: ARTConnectionProtocol, Sendable {}
2931

3032
/// Like (a subset of) `ARTRealtimeChannelOptions` but with value semantics. (It’s unfortunate that `ARTRealtimeChannelOptions` doesn’t have a `-copy` method.)

Sources/AblyChat/RoomFeature.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ internal protocol FeatureChannel: Sendable, EmitsDiscontinuities {
6363

6464
internal struct DefaultFeatureChannel: FeatureChannel {
6565
internal var channel: RealtimeChannelProtocol
66-
internal var contributor: DefaultRoomLifecycleContributor
66+
internal var contributor: any RoomLifecycleContributor & EmitsDiscontinuities
6767
internal var roomLifecycleManager: RoomLifecycleManager
6868

6969
internal func onDiscontinuity(bufferingPolicy: BufferingPolicy) async -> Subscription<DiscontinuityEvent> {

Tests/AblyChatTests/DefaultMessagesTests.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ struct DefaultMessagesTests {
1010
// Given
1111
let realtime = MockRealtime.create()
1212
let chatAPI = ChatAPI(realtime: realtime)
13-
let channel = MockRealtimeChannel()
13+
let channel = MockRealtimeChannel(attachResult: .success)
1414
let featureChannel = MockFeatureChannel(channel: channel)
1515
let defaultMessages = await DefaultMessages(featureChannel: featureChannel, chatAPI: chatAPI, roomID: "basketball", clientID: "clientId", logger: TestLogger())
1616

@@ -28,7 +28,7 @@ struct DefaultMessagesTests {
2828
// Given
2929
let realtime = MockRealtime.create { (MockHTTPPaginatedResponse.successGetMessagesWithNoItems, nil) }
3030
let chatAPI = ChatAPI(realtime: realtime)
31-
let channel = MockRealtimeChannel()
31+
let channel = MockRealtimeChannel(attachResult: .success)
3232
let featureChannel = MockFeatureChannel(channel: channel)
3333
let defaultMessages = await DefaultMessages(featureChannel: featureChannel, chatAPI: chatAPI, roomID: "basketball", clientID: "clientId", logger: TestLogger())
3434

@@ -52,7 +52,8 @@ struct DefaultMessagesTests {
5252
properties: .init(
5353
attachSerial: "001",
5454
channelSerial: "001"
55-
)
55+
),
56+
attachResult: .success
5657
)
5758
let featureChannel = MockFeatureChannel(channel: channel)
5859
let defaultMessages = await DefaultMessages(featureChannel: featureChannel, chatAPI: chatAPI, roomID: "basketball", clientID: "clientId", logger: TestLogger())
@@ -80,6 +81,7 @@ struct DefaultMessagesTests {
8081
attachSerial: "001",
8182
channelSerial: "001"
8283
),
84+
attachResult: .success,
8385
messageToEmitOnSubscribe: .init(
8486
action: .create, // arbitrary
8587
serial: "", // arbitrary
@@ -114,6 +116,7 @@ struct DefaultMessagesTests {
114116
attachSerial: "001",
115117
channelSerial: "001"
116118
),
119+
attachResult: .success,
117120
messageToEmitOnSubscribe: .init(
118121
action: .create, // arbitrary
119122
serial: "", // arbitrary
@@ -142,7 +145,7 @@ struct DefaultMessagesTests {
142145
// Given: A DefaultMessages instance
143146
let realtime = MockRealtime.create()
144147
let chatAPI = ChatAPI(realtime: realtime)
145-
let channel = MockRealtimeChannel()
148+
let channel = MockRealtimeChannel(attachResult: .success)
146149
let featureChannel = MockFeatureChannel(channel: channel)
147150
let messages = await DefaultMessages(featureChannel: featureChannel, chatAPI: chatAPI, roomID: "basketball", clientID: "clientId", logger: TestLogger())
148151

Tests/AblyChatTests/DefaultRoomPresenceTests.swift

Lines changed: 486 additions & 0 deletions
Large diffs are not rendered by default.

Tests/AblyChatTests/Helpers/Helpers.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,62 @@ func isChatError(_ maybeError: (any Error)?, withCodeAndStatusCode codeAndStatus
2121
return ablyError.message == message
2222
}()
2323
}
24+
25+
extension ARTPresenceMessage {
26+
convenience init(clientId: String, data: Any? = [:], timestamp: Date = Date()) {
27+
self.init()
28+
self.clientId = clientId
29+
self.data = data
30+
self.timestamp = timestamp
31+
}
32+
}
33+
34+
extension [PresenceEventType] {
35+
static let all = [
36+
PresenceEventType.present,
37+
PresenceEventType.enter,
38+
PresenceEventType.leave,
39+
PresenceEventType.update,
40+
]
41+
}
42+
43+
enum RoomLifecycleHelper {
44+
static let fakeNetworkDelay: UInt64 = 10 // milliseconds; without this delay (or with a very low value such as 1) most of the time attach happens before lifecycleManager has a chance to start waiting.
45+
46+
static func createManager(
47+
forTestingWhatHappensWhenCurrentlyIn status: DefaultRoomLifecycleManager<MockRoomLifecycleContributor>.Status? = nil,
48+
forTestingWhatHappensWhenHasPendingDiscontinuityEvents pendingDiscontinuityEvents: [MockRoomLifecycleContributor.ID: DiscontinuityEvent]? = nil,
49+
forTestingWhatHappensWhenHasTransientDisconnectTimeoutForTheseContributorIDs idsOfContributorsWithTransientDisconnectTimeout: Set<MockRoomLifecycleContributor.ID>? = nil,
50+
contributors: [MockRoomLifecycleContributor] = [],
51+
clock: SimpleClock = MockSimpleClock()
52+
) async -> DefaultRoomLifecycleManager<MockRoomLifecycleContributor> {
53+
await .init(
54+
testsOnly_status: status,
55+
testsOnly_pendingDiscontinuityEvents: pendingDiscontinuityEvents,
56+
testsOnly_idsOfContributorsWithTransientDisconnectTimeout: idsOfContributorsWithTransientDisconnectTimeout,
57+
contributors: contributors,
58+
logger: TestLogger(),
59+
clock: clock
60+
)
61+
}
62+
63+
static func createContributor(
64+
initialState: ARTRealtimeChannelState = .initialized,
65+
initialErrorReason: ARTErrorInfo? = nil,
66+
feature: RoomFeature = .messages, // Arbitrarily chosen, its value only matters in test cases where we check which error is thrown
67+
attachBehavior: MockRoomLifecycleContributorChannel.AttachOrDetachBehavior? = nil,
68+
detachBehavior: MockRoomLifecycleContributorChannel.AttachOrDetachBehavior? = nil,
69+
subscribeToStateBehavior: MockRoomLifecycleContributorChannel.SubscribeToStateBehavior? = nil
70+
) -> MockRoomLifecycleContributor {
71+
.init(
72+
feature: feature,
73+
channel: .init(
74+
initialState: initialState,
75+
initialErrorReason: initialErrorReason,
76+
attachBehavior: attachBehavior,
77+
detachBehavior: detachBehavior,
78+
subscribeToStateBehavior: subscribeToStateBehavior
79+
)
80+
)
81+
}
82+
}

Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ import Ably
22
import AblyChat
33

44
final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
5-
var presence: ARTRealtimePresenceProtocol {
6-
fatalError("Not implemented")
7-
}
8-
95
private let attachSerial: String?
106
private let channelSerial: String?
117
private let _name: String?
8+
private let mockPresence: MockRealtimePresence!
129

1310
var properties: ARTChannelProperties { .init(attachSerial: attachSerial, channelSerial: channelSerial) }
1411

12+
var presence: ARTRealtimePresenceProtocol { mockPresence }
13+
1514
// I don't see why the nonisolated(unsafe) keyword would cause a problem when used for tests in this context.
1615
nonisolated(unsafe) var lastMessagePublishedName: String?
1716
nonisolated(unsafe) var lastMessagePublishedData: Any?
@@ -32,14 +31,16 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
3231
state _: ARTRealtimeChannelState = .suspended,
3332
attachResult: AttachOrDetachResult? = nil,
3433
detachResult: AttachOrDetachResult? = nil,
35-
messageToEmitOnSubscribe: MessageToEmit? = nil
34+
messageToEmitOnSubscribe: MessageToEmit? = nil,
35+
mockPresence: MockRealtimePresence! = nil
3636
) {
3737
_name = name
3838
self.attachResult = attachResult
3939
self.detachResult = detachResult
4040
self.messageToEmitOnSubscribe = messageToEmitOnSubscribe
4141
attachSerial = properties.attachSerial
4242
channelSerial = properties.channelSerial
43+
self.mockPresence = mockPresence
4344
}
4445

4546
/// A threadsafe counter that starts at zero.
@@ -71,7 +72,7 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
7172
}
7273

7374
var state: ARTRealtimeChannelState {
74-
.attached
75+
attachResult == .success ? .attached : .failed
7576
}
7677

7778
var errorReason: ARTErrorInfo? {
@@ -86,7 +87,7 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
8687
fatalError("Not implemented")
8788
}
8889

89-
enum AttachOrDetachResult {
90+
enum AttachOrDetachResult: Equatable {
9091
case success
9192
case failure(ARTErrorInfo)
9293

@@ -183,7 +184,7 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
183184
}
184185

185186
func on(_: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener {
186-
fatalError("Not implemented")
187+
ARTEventListener()
187188
}
188189

189190
func once(_: ARTChannelEvent, callback _: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import Ably
2+
import AblyChat
3+
4+
final class MockRealtimePresence: NSObject, @unchecked Sendable, RealtimePresenceProtocol {
5+
let syncComplete: Bool
6+
private var members: [ARTPresenceMessage]
7+
private var currentMember: ARTPresenceMessage?
8+
private var subscribeCallback: ARTPresenceMessageCallback?
9+
private var presenceGetError: ARTErrorInfo?
10+
11+
init(syncComplete: Bool = true, members: [ARTPresenceMessage], presenceGetError: ARTErrorInfo? = nil) {
12+
self.syncComplete = syncComplete
13+
self.members = members
14+
currentMember = members.count == 1 ? members[0] : nil
15+
self.presenceGetError = presenceGetError
16+
}
17+
18+
func get(_ callback: @escaping ARTPresenceMessagesCallback) {
19+
callback(presenceGetError == nil ? members : nil, presenceGetError)
20+
}
21+
22+
func get(_ query: ARTRealtimePresenceQuery, callback: @escaping ARTPresenceMessagesCallback) {
23+
callback(members.filter { $0.clientId == query.clientId }, nil)
24+
}
25+
26+
func enter(_: Any?) {
27+
fatalError("Not implemented")
28+
}
29+
30+
func enter(_: Any?, callback _: ARTCallback? = nil) {
31+
fatalError("Not implemented")
32+
}
33+
34+
func update(_ data: Any?) {
35+
currentMember?.data = data
36+
}
37+
38+
func update(_ data: Any?, callback: ARTCallback? = nil) {
39+
currentMember?.data = data
40+
callback?(nil)
41+
}
42+
43+
func leave(_: Any?) {
44+
members.removeAll { $0.clientId == currentMember?.clientId }
45+
}
46+
47+
func leave(_: Any?, callback: ARTCallback? = nil) {
48+
members.removeAll { $0.clientId == currentMember?.clientId }
49+
callback?(nil)
50+
}
51+
52+
func enterClient(_ clientId: String, data: Any?) {
53+
currentMember = ARTPresenceMessage(clientId: clientId, data: data)
54+
members.append(currentMember!)
55+
currentMember!.action = .enter
56+
subscribeCallback?(currentMember!)
57+
}
58+
59+
func enterClient(_ clientId: String, data: Any?, callback: ARTCallback? = nil) {
60+
currentMember = ARTPresenceMessage(clientId: clientId, data: data)
61+
members.append(currentMember!)
62+
callback?(nil)
63+
currentMember!.action = .enter
64+
subscribeCallback?(currentMember!)
65+
}
66+
67+
func updateClient(_ clientId: String, data: Any?) {
68+
members.first { $0.clientId == clientId }?.data = data
69+
}
70+
71+
func updateClient(_ clientId: String, data: Any?, callback: ARTCallback? = nil) {
72+
guard let member = members.first(where: { $0.clientId == clientId }) else {
73+
preconditionFailure("Client \(clientId) doesn't exist in this presence set.")
74+
}
75+
member.action = .update
76+
member.data = data
77+
subscribeCallback?(member)
78+
callback?(nil)
79+
}
80+
81+
func leaveClient(_ clientId: String, data _: Any?) {
82+
members.removeAll { $0.clientId == clientId }
83+
}
84+
85+
func leaveClient(_ clientId: String, data _: Any?, callback _: ARTCallback? = nil) {
86+
members.removeAll { $0.clientId == clientId }
87+
}
88+
89+
func subscribe(_ callback: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
90+
subscribeCallback = callback
91+
for member in members {
92+
subscribeCallback?(member)
93+
}
94+
return ARTEventListener()
95+
}
96+
97+
func subscribe(attachCallback _: ARTCallback?, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
98+
ARTEventListener()
99+
}
100+
101+
func subscribe(_: ARTPresenceAction, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
102+
ARTEventListener()
103+
}
104+
105+
func subscribe(_: ARTPresenceAction, onAttach _: ARTCallback?, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
106+
ARTEventListener()
107+
}
108+
109+
func unsubscribe() {
110+
fatalError("Not implemented")
111+
}
112+
113+
func unsubscribe(_: ARTEventListener) {
114+
subscribeCallback = nil
115+
}
116+
117+
func unsubscribe(_: ARTPresenceAction, listener _: ARTEventListener) {
118+
fatalError("Not implemented")
119+
}
120+
121+
func history(_: @escaping ARTPaginatedPresenceCallback) {
122+
fatalError("Not implemented")
123+
}
124+
125+
func history(_: ARTRealtimeHistoryQuery?, callback _: @escaping ARTPaginatedPresenceCallback) throws {
126+
fatalError("Not implemented")
127+
}
128+
}

Tests/AblyChatTests/Mocks/MockRoomLifecycleContributor.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Ably
22
@testable import AblyChat
33

4-
actor MockRoomLifecycleContributor: RoomLifecycleContributor {
4+
actor MockRoomLifecycleContributor: RoomLifecycleContributor, EmitsDiscontinuities {
55
nonisolated let feature: RoomFeature
66
nonisolated let channel: MockRoomLifecycleContributorChannel
77

@@ -15,4 +15,8 @@ actor MockRoomLifecycleContributor: RoomLifecycleContributor {
1515
func emitDiscontinuity(_ discontinuity: DiscontinuityEvent) async {
1616
emitDiscontinuityArguments.append(discontinuity)
1717
}
18+
19+
func onDiscontinuity(bufferingPolicy _: AblyChat.BufferingPolicy) async -> AblyChat.Subscription<AblyChat.DiscontinuityEvent> {
20+
fatalError("Not implemented")
21+
}
1822
}

Tests/AblyChatTests/Mocks/MockRoomLifecycleContributorChannel.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ final actor MockRoomLifecycleContributorChannel: RoomLifecycleContributorChannel
4545
/// Receives an argument indicating how many times (including the current call) the method for which this is providing a mock implementation has been called.
4646
case fromFunction(@Sendable (Int) async -> AttachOrDetachBehavior)
4747
case complete(AttachOrDetachResult)
48-
case completeAndChangeState(AttachOrDetachResult, newState: ARTRealtimeChannelState)
48+
case completeAndChangeState(AttachOrDetachResult, newState: ARTRealtimeChannelState, delayInMilliseconds: UInt64 = 0) // emulating network delay before going to the new state
4949

5050
static var success: Self {
5151
.complete(.success)
@@ -90,7 +90,10 @@ final actor MockRoomLifecycleContributorChannel: RoomLifecycleContributorChannel
9090
return
9191
case let .complete(completeResult):
9292
result = completeResult
93-
case let .completeAndChangeState(completeResult, newState):
93+
case let .completeAndChangeState(completeResult, newState, milliseconds):
94+
if milliseconds > 0 {
95+
try! await Task.sleep(nanoseconds: milliseconds * 1_000_000)
96+
}
9497
state = newState
9598
if case let .failure(error) = completeResult {
9699
errorReason = error

0 commit comments

Comments
 (0)