Skip to content

Commit be42380

Browse files
committed
Add presence tests without using DefaultRoomLifecycleManager.
1 parent f86f569 commit be42380

File tree

8 files changed

+618
-50
lines changed

8 files changed

+618
-50
lines changed

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: any 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()
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 { (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
@@ -116,6 +118,7 @@ struct DefaultMessagesTests {
116118
attachSerial: "001",
117119
channelSerial: "001"
118120
),
121+
attachResult: .success,
119122
messageToEmitOnSubscribe: .init(
120123
action: .create, // arbitrary
121124
serial: "", // arbitrary
@@ -146,7 +149,7 @@ struct DefaultMessagesTests {
146149
// Given: A DefaultMessages instance
147150
let realtime = MockRealtime()
148151
let chatAPI = ChatAPI(realtime: realtime)
149-
let channel = MockRealtimeChannel()
152+
let channel = MockRealtimeChannel(attachResult: .success)
150153
let featureChannel = MockFeatureChannel(channel: channel)
151154
let messages = await DefaultMessages(featureChannel: featureChannel, chatAPI: chatAPI, roomID: "basketball", clientID: "clientId", logger: TestLogger())
152155

Tests/AblyChatTests/DefaultRoomPresenceTests.swift

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

Tests/AblyChatTests/Helpers/Helpers.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,41 @@ 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+
func createMockContributor(
35+
initialState: ARTRealtimeChannelState = .initialized,
36+
initialErrorReason: ARTErrorInfo? = nil,
37+
feature: RoomFeature = .messages, // Arbitrarily chosen, its value only matters in test cases where we check which error is thrown
38+
attachBehavior: MockRoomLifecycleContributorChannel.AttachOrDetachBehavior? = nil,
39+
detachBehavior: MockRoomLifecycleContributorChannel.AttachOrDetachBehavior? = nil,
40+
subscribeToStateBehavior: MockRoomLifecycleContributorChannel.SubscribeToStateBehavior? = nil
41+
) -> MockRoomLifecycleContributor {
42+
.init(
43+
feature: feature,
44+
channel: .init(
45+
initialState: initialState,
46+
initialErrorReason: initialErrorReason,
47+
attachBehavior: attachBehavior,
48+
detachBehavior: detachBehavior,
49+
subscribeToStateBehavior: subscribeToStateBehavior
50+
)
51+
)
52+
}
53+
54+
extension [PresenceEventType] {
55+
static let all = [
56+
PresenceEventType.present,
57+
PresenceEventType.enter,
58+
PresenceEventType.leave,
59+
PresenceEventType.update,
60+
]
61+
}

Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift

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

44
final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
5-
let presence = MockRealtimePresence()
6-
75
private let attachSerial: String?
86
private let channelSerial: String?
97
private let _name: String?
8+
private let mockPresence: MockRealtimePresence!
109

1110
var properties: ARTChannelProperties { .init(attachSerial: attachSerial, channelSerial: channelSerial) }
1211

12+
var presence: MockRealtimePresence { mockPresence }
13+
1314
// I don't see why the nonisolated(unsafe) keyword would cause a problem when used for tests in this context.
1415
nonisolated(unsafe) var lastMessagePublishedName: String?
1516
nonisolated(unsafe) var lastMessagePublishedData: Any?
@@ -32,14 +33,16 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
3233
state _: ARTRealtimeChannelState = .suspended,
3334
attachResult: AttachOrDetachResult? = nil,
3435
detachResult: AttachOrDetachResult? = nil,
35-
messageToEmitOnSubscribe: MessageToEmit? = nil
36+
messageToEmitOnSubscribe: MessageToEmit? = nil,
37+
mockPresence: MockRealtimePresence! = nil
3638
) {
3739
_name = name
3840
self.attachResult = attachResult
3941
self.detachResult = detachResult
4042
self.messageToEmitOnSubscribe = messageToEmitOnSubscribe
4143
attachSerial = properties.attachSerial
4244
channelSerial = properties.channelSerial
45+
self.mockPresence = mockPresence
4346
}
4447

4548
/// A threadsafe counter that starts at zero.
@@ -71,7 +74,7 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
7174
}
7275

7376
var state: ARTRealtimeChannelState {
74-
.attached
77+
attachResult == .success ? .attached : .failed
7578
}
7679

7780
var errorReason: ARTErrorInfo? {
@@ -86,7 +89,7 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
8689
fatalError("Not implemented")
8790
}
8891

89-
enum AttachOrDetachResult {
92+
enum AttachOrDetachResult: Equatable {
9093
case success
9194
case failure(ARTErrorInfo)
9295

@@ -185,7 +188,7 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
185188
}
186189

187190
func on(_: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener {
188-
fatalError("Not implemented")
191+
ARTEventListener()
189192
}
190193

191194
func once(_: ARTChannelEvent, callback _: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener {

Tests/AblyChatTests/Mocks/MockRealtimePresence.swift

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
import Ably
22
import AblyChat
33

4-
final class MockRealtimePresence: RealtimePresenceProtocol {
5-
var syncComplete: Bool {
6-
fatalError("Not implemented")
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
716
}
817

9-
func get(_: @escaping ARTPresenceMessagesCallback) {
10-
fatalError("Not implemented")
18+
func get(_ callback: @escaping ARTPresenceMessagesCallback) {
19+
callback(presenceGetError == nil ? members : nil, presenceGetError)
1120
}
1221

13-
func get(_: ARTRealtimePresenceQuery, callback _: @escaping ARTPresenceMessagesCallback) {
14-
fatalError("Not implemented")
22+
func get(_ query: ARTRealtimePresenceQuery, callback: @escaping ARTPresenceMessagesCallback) {
23+
callback(members.filter { $0.clientId == query.clientId }, nil)
1524
}
1625

1726
func enter(_: Any?) {
@@ -22,68 +31,87 @@ final class MockRealtimePresence: RealtimePresenceProtocol {
2231
fatalError("Not implemented")
2332
}
2433

25-
func update(_: Any?) {
26-
fatalError("Not implemented")
34+
func update(_ data: Any?) {
35+
currentMember?.data = data
2736
}
2837

29-
func update(_: Any?, callback _: ARTCallback? = nil) {
30-
fatalError("Not implemented")
38+
func update(_ data: Any?, callback: ARTCallback? = nil) {
39+
currentMember?.data = data
40+
callback?(nil)
3141
}
3242

3343
func leave(_: Any?) {
34-
fatalError("Not implemented")
44+
members.removeAll { $0.clientId == currentMember?.clientId }
3545
}
3646

37-
func leave(_: Any?, callback _: ARTCallback? = nil) {
38-
fatalError("Not implemented")
47+
func leave(_: Any?, callback: ARTCallback? = nil) {
48+
members.removeAll { $0.clientId == currentMember?.clientId }
49+
callback?(nil)
3950
}
4051

41-
func enterClient(_: String, data _: Any?) {
42-
fatalError("Not implemented")
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!)
4357
}
4458

45-
func enterClient(_: String, data _: Any?, callback _: ARTCallback? = nil) {
46-
fatalError("Not implemented")
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!)
4765
}
4866

49-
func updateClient(_: String, data _: Any?) {
50-
fatalError("Not implemented")
67+
func updateClient(_ clientId: String, data: Any?) {
68+
members.first { $0.clientId == clientId }?.data = data
5169
}
5270

53-
func updateClient(_: String, data _: Any?, callback _: ARTCallback? = nil) {
54-
fatalError("Not implemented")
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)
5579
}
5680

57-
func leaveClient(_: String, data _: Any?) {
58-
fatalError("Not implemented")
81+
func leaveClient(_ clientId: String, data _: Any?) {
82+
members.removeAll { $0.clientId == clientId }
5983
}
6084

61-
func leaveClient(_: String, data _: Any?, callback _: ARTCallback? = nil) {
62-
fatalError("Not implemented")
85+
func leaveClient(_ clientId: String, data _: Any?, callback _: ARTCallback? = nil) {
86+
members.removeAll { $0.clientId == clientId }
6387
}
6488

65-
func subscribe(_: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
66-
fatalError("Not implemented")
89+
func subscribe(_ callback: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
90+
subscribeCallback = callback
91+
for member in members {
92+
subscribeCallback?(member)
93+
}
94+
return ARTEventListener()
6795
}
6896

6997
func subscribe(attachCallback _: ARTCallback?, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
70-
fatalError("Not implemented")
98+
ARTEventListener()
7199
}
72100

73101
func subscribe(_: ARTPresenceAction, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
74-
fatalError("Not implemented")
102+
ARTEventListener()
75103
}
76104

77105
func subscribe(_: ARTPresenceAction, onAttach _: ARTCallback?, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
78-
fatalError("Not implemented")
106+
ARTEventListener()
79107
}
80108

81109
func unsubscribe() {
82110
fatalError("Not implemented")
83111
}
84112

85113
func unsubscribe(_: ARTEventListener) {
86-
fatalError("Not implemented")
114+
subscribeCallback = nil
87115
}
88116

89117
func unsubscribe(_: ARTPresenceAction, listener _: ARTEventListener) {

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/MockRoomLifecycleManager.swift

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,36 @@ import Ably
33

44
actor MockRoomLifecycleManager: RoomLifecycleManager {
55
private let attachResult: Result<Void, ARTErrorInfo>?
6-
private(set) var attachCallCount = 0
76
private let detachResult: Result<Void, ARTErrorInfo>?
7+
private let resultOfWaitToBeAbleToPerformPresenceOperations: Result<Void, ARTErrorInfo>?
8+
9+
private(set) var attachCallCount = 0
810
private(set) var detachCallCount = 0
911
private(set) var releaseCallCount = 0
12+
private(set) var waitCallCount = 0
13+
1014
private let _roomStatus: RoomStatus?
1115
private var subscriptions = SubscriptionStorage<RoomStatusChange>()
1216

13-
init(attachResult: Result<Void, ARTErrorInfo>? = nil, detachResult: Result<Void, ARTErrorInfo>? = nil, roomStatus: RoomStatus? = nil) {
17+
init(attachResult: Result<Void, ARTErrorInfo>? = nil,
18+
detachResult: Result<Void, ARTErrorInfo>? = nil,
19+
roomStatus: RoomStatus? = nil,
20+
resultOfWaitToBeAblePerformPresenceOperations: Result<Void, ARTErrorInfo>? = nil) {
1421
self.attachResult = attachResult
1522
self.detachResult = detachResult
23+
resultOfWaitToBeAbleToPerformPresenceOperations = resultOfWaitToBeAblePerformPresenceOperations
1624
_roomStatus = roomStatus
1725
}
1826

1927
func performAttachOperation() async throws {
2028
attachCallCount += 1
21-
guard let attachResult else {
22-
fatalError("In order to call performAttachOperation, attachResult must be passed to the initializer")
29+
emitStatusChange(.init(current: .attaching(error: nil), previous: _roomStatus ?? .initialized))
30+
if resultOfWaitToBeAbleToPerformPresenceOperations == nil {
31+
guard let attachResult else {
32+
fatalError("In order to call performAttachOperation, attachResult must be passed to the initializer")
33+
}
34+
try attachResult.get()
2335
}
24-
try attachResult.get()
2536
}
2637

2738
func performDetachOperation() async throws {
@@ -52,6 +63,10 @@ actor MockRoomLifecycleManager: RoomLifecycleManager {
5263
}
5364

5465
func waitToBeAbleToPerformPresenceOperations(requestedByFeature _: RoomFeature) async throws(ARTErrorInfo) {
55-
fatalError("Not implemented")
66+
waitCallCount += 1
67+
guard let resultOfWaitToBeAbleToPerformPresenceOperations else {
68+
fatalError("resultOfWaitToBeAblePerformPresenceOperations must be set before waitToBeAbleToPerformPresenceOperations is called")
69+
}
70+
try resultOfWaitToBeAbleToPerformPresenceOperations.get()
5671
}
5772
}

0 commit comments

Comments
 (0)