-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathMockRealtimeChannel.swift
More file actions
216 lines (175 loc) · 7.32 KB
/
MockRealtimeChannel.swift
File metadata and controls
216 lines (175 loc) · 7.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import Ably
@testable import AblyChat
final class MockRealtimeChannel: InternalRealtimeChannelProtocol {
let presence: MockRealtimePresence
let annotations: MockRealtimeAnnotations
let proxied = MockAblyCocoaRealtime.Channel()
private let attachSerial: String?
private let channelSerial: String?
private let _name: String?
var properties: ARTChannelProperties { .init(attachSerial: attachSerial, channelSerial: channelSerial) }
private var _state: ARTRealtimeChannelState?
private let stateChangeToEmitForListener: ChannelStateChange?
var errorReason: ErrorInfo?
var publishedMessages: [TestMessage] = []
struct TestMessage {
let name: String?
let data: JSONValue?
let extras: [String: JSONValue]?
}
init(
name: String? = nil,
properties: ARTChannelProperties = .init(),
initialState: ARTRealtimeChannelState? = nil,
initialErrorReason: ErrorInfo? = nil,
attachBehavior: AttachOrDetachBehavior? = nil,
detachBehavior: AttachOrDetachBehavior? = nil,
messageToEmitOnSubscribe: ARTMessage? = nil,
annotationToEmitOnSubscribe: ARTAnnotation? = nil,
stateChangeToEmitForListener: ChannelStateChange? = nil,
) {
_name = name
_state = initialState
self.attachBehavior = attachBehavior
self.detachBehavior = detachBehavior
errorReason = initialErrorReason
self.messageToEmitOnSubscribe = messageToEmitOnSubscribe
attachSerial = properties.attachSerial
channelSerial = properties.channelSerial
self.stateChangeToEmitForListener = stateChangeToEmitForListener
presence = MockRealtimePresence()
annotations = MockRealtimeAnnotations(annotationToEmitOnSubscribe: annotationToEmitOnSubscribe)
}
var state: ARTRealtimeChannelState {
guard let state = _state else {
fatalError("Channel state not set")
}
return state
}
var underlying: any RealtimeChannelProtocol {
fatalError("Not implemented")
}
enum AttachOrDetachBehavior {
/// Receives an argument indicating how many times (including the current call) the method for which this is providing a mock implementation has been called.
case fromFunction(@Sendable (Int) async -> AttachOrDetachBehavior)
case complete(AttachOrDetachResult)
case completeAndChangeState(AttachOrDetachResult, newState: ARTRealtimeChannelState)
static var success: Self {
.complete(.success)
}
static func failure(_ error: ErrorInfo) -> Self {
.complete(.failure(error))
}
}
enum AttachOrDetachResult {
case success
case failure(ErrorInfo)
func get() throws(ErrorInfo) {
switch self {
case .success:
break
case let .failure(error):
throw error
}
}
}
private let attachBehavior: AttachOrDetachBehavior?
var attachCallCount = 0
func attach() async throws(ErrorInfo) {
attachCallCount += 1
guard let attachBehavior else {
fatalError("attachBehavior must be set before attach is called")
}
try await performBehavior(attachBehavior, callCount: attachCallCount)
}
private let detachBehavior: AttachOrDetachBehavior?
var detachCallCount = 0
func detach() async throws(ErrorInfo) {
detachCallCount += 1
guard let detachBehavior else {
fatalError("detachBehavior must be set before detach is called")
}
try await performBehavior(detachBehavior, callCount: detachCallCount)
}
private func performBehavior(_ behavior: AttachOrDetachBehavior, callCount: Int) async throws(ErrorInfo) {
let result: AttachOrDetachResult
switch behavior {
case let .fromFunction(function):
let behavior = await function(callCount)
try await performBehavior(behavior, callCount: callCount)
return
case let .complete(completeResult):
result = completeResult
case let .completeAndChangeState(completeResult, newState):
_state = newState
if case let .failure(error) = completeResult {
errorReason = error
}
result = completeResult
}
try result.get()
}
let messageToEmitOnSubscribe: ARTMessage?
private var channelSubscriptions: [(String, (ARTMessage) -> Void)] = []
func subscribe(_ callback: @escaping @MainActor @Sendable (ARTMessage) -> Void) -> ARTEventListener? {
subscribe("all", callback: callback) // "all" is arbitrary here, could be "". Due to `name` is not optional.
}
// Added the ability to emit a message whenever we want instead of just on subscribe... I didn't want to dig into what the messageToEmitOnSubscribe is too much so just if/else between the two.
func subscribe(_ name: String, callback: @escaping @MainActor (ARTMessage) -> Void) -> ARTEventListener? {
if let messageToEmitOnSubscribe {
callback(messageToEmitOnSubscribe)
}
channelSubscriptions.append((name, callback))
return ARTEventListener()
}
func simulateIncomingMessage(_ with: ARTMessage, for name: String) {
for (messageName, callback) in channelSubscriptions where messageName == name {
callback(with)
}
}
func unsubscribe(_: ARTEventListener?) {
channelSubscriptions.removeAll() // make more strict when needed
}
private var stateSubscriptionCallbacks: [@MainActor (ChannelStateChange) -> Void] = []
func on(_: ARTChannelEvent, callback: @escaping @MainActor (ChannelStateChange) -> Void) -> ARTEventListener {
stateSubscriptionCallbacks.append(callback)
return ARTEventListener()
}
func on(_ callback: @escaping @MainActor (ChannelStateChange) -> Void) -> ARTEventListener {
stateSubscriptionCallbacks.append(callback)
if let stateChangeToEmitForListener {
callback(stateChangeToEmitForListener)
}
return ARTEventListener()
}
func once(_ callback: @escaping @MainActor @Sendable (ChannelStateChange) -> Void) -> ARTEventListener {
stateSubscriptionCallbacks.append(callback)
if let stateChangeToEmitForListener {
callback(stateChangeToEmitForListener)
}
return ARTEventListener()
}
func once(_: ARTChannelEvent, callback _: @escaping @MainActor @Sendable (ChannelStateChange) -> Void) -> ARTEventListener {
fatalError("Not implemented")
}
func emitEvent(_ event: ChannelStateChange) {
for callback in stateSubscriptionCallbacks {
callback(event)
}
}
func off(_: ARTEventListener) {
// no-op; revisit if we need to test something that depends on this method actually stopping `on` from emitting more events
}
var name: String {
guard let name = _name else {
fatalError("Channel name not set")
}
return name
}
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)
}
}