Skip to content

Commit 5311848

Browse files
committed
Add tests for Connection.
1 parent 18ae5bd commit 5311848

File tree

3 files changed

+188
-8
lines changed

3 files changed

+188
-8
lines changed

Sources/AblyChat/Connection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public enum ConnectionStatus: Sendable {
108108
*/
109109
case failed
110110

111-
// (CHA-CS1g) The @CLOSING@ status is used when a user has called @close@ on the underlying Realtime client and it is attempting to close the connection with Ably. The library will not attempt to reconnect.
111+
// (CHA-CS1g) The CLOSING status is used when a user has called close on the underlying Realtime client and it is attempting to close the connection with Ably. The library will not attempt to reconnect.
112112

113113
/**
114114
* An explicit request by the developer to close the connection has been sent to the Ably service.
@@ -117,7 +117,7 @@ public enum ConnectionStatus: Sendable {
117117
*/
118118
case closing
119119

120-
// (CHA-CS1h) The @CLOSED@ status is used when the @close@ call on the underlying Realtime client has succeeded, either via mutual agreement with the server or forced after a time out. The library will not attempt to reconnect.
120+
// (CHA-CS1h) The CLOSED status is used when the close call on the underlying Realtime client has succeeded, either via mutual agreement with the server or forced after a time out. The library will not attempt to reconnect.
121121

122122
/**
123123
* The connection has been explicitly closed by the client.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import Ably
2+
@testable import AblyChat
3+
import Testing
4+
5+
@MainActor
6+
struct DefaultConnectionTests {
7+
// MARK: - CHA-CS1: Connection Status Values
8+
9+
// These specs are not marked as `[Testable]`, but lets have this basic check anyway:
10+
// CHA-CS1a
11+
// CHA-CS1b
12+
// CHA-CS1c
13+
// CHA-CS1d
14+
// CHA-CS1e
15+
// CHA-CS1f
16+
// CHA-CS1g
17+
// CHA-CS1h
18+
@Test
19+
func chatConnectionStatusReflectsAllRealtimeConnectionStates() async throws {
20+
// Test all possible realtime connection state mappings to chat connection status
21+
let testCases: [(ARTRealtimeConnectionState, ConnectionStatus, String)] = [
22+
// CHA-CS1a: INITIALIZED status
23+
(.initialized, .initialized, "initialized"),
24+
// CHA-CS1b: CONNECTING status
25+
(.connecting, .connecting, "connecting"),
26+
// CHA-CS1c: CONNECTED status
27+
(.connected, .connected, "connected"),
28+
// CHA-CS1d: DISCONNECTED status
29+
(.disconnected, .disconnected, "disconnected"),
30+
// CHA-CS1e: SUSPENDED status
31+
(.suspended, .suspended, "suspended"),
32+
// CHA-CS1f: FAILED status
33+
(.failed, .failed, "failed"),
34+
// CHA-CS1g: CLOSING status
35+
(.closing, .closing, "closing"),
36+
// CHA-CS1h: CLOSED status
37+
(.closed, .closed, "closed"),
38+
]
39+
40+
for (realtimeState, expectedChatStatus, description) in testCases {
41+
// Given: A connection in a specific realtime state
42+
let mockConnection = MockConnection(state: realtimeState)
43+
let mockRealtime = MockRealtime(connection: mockConnection)
44+
let connection = DefaultConnection(realtime: mockRealtime)
45+
46+
// When: The connection status is checked
47+
let status = connection.status
48+
49+
// Then: Status should match the expected chat connection status
50+
#expect(status == expectedChatStatus, "Realtime state \(description) should map to \(expectedChatStatus)")
51+
}
52+
}
53+
54+
// MARK: - CHA-CS2: Exposing Connection Status and Error
55+
56+
// @spec CHA-CS2a
57+
// @spec CHA-CS2b
58+
// @spec CHA-CS3
59+
@Test
60+
func chatClientMustExposeItsCurrentStatusAndError() async throws {
61+
// Given: An instance of ChatClient with initialized connection and no error
62+
let options = ARTClientOptions(key: "fake:key")
63+
options.autoConnect = false
64+
let realtime = ARTRealtime(options: options)
65+
let client = ChatClient(realtime: realtime, clientOptions: nil)
66+
67+
// When: The connection status and error are checked
68+
let status = client.connection.status
69+
let error = client.connection.error
70+
71+
// Then: Status should be initialized and error should be nil (CHA-CS3)
72+
#expect(status == .initialized)
73+
#expect(error == nil)
74+
}
75+
76+
// @spec CHA-CS2b
77+
@Test
78+
func chatClientMustExposeLatestError() async throws {
79+
// Given: A connection with an error
80+
let connectionError = ErrorInfo(
81+
code: 40142,
82+
message: "Connection failed",
83+
statusCode: 401,
84+
)
85+
let mockConnection = MockConnection(state: .failed, errorReason: connectionError)
86+
let mockRealtime = MockRealtime(connection: mockConnection)
87+
let connection = DefaultConnection(realtime: mockRealtime)
88+
89+
// When: The error is checked
90+
let error = connection.error
91+
92+
// Then: The error should match the connection error
93+
#expect(error?.code == connectionError.code)
94+
#expect(error?.message == connectionError.message)
95+
#expect(error?.statusCode == connectionError.statusCode)
96+
}
97+
98+
// MARK: - CHA-CS4: Observing Connection Status
99+
100+
// @spec CHA-CS4a
101+
// @spec CHA-CS4b
102+
// @spec CHA-CS4c
103+
// @spec CHA-CS4d
104+
// @spec CHA-CS4e
105+
// @spec CHA-CS5c - mocks are the same as for CHA-CS4, so CHA-CS5c is covered by this test
106+
@Test
107+
func clientsCanRegisterListenerForConnectionStatusEvents() async throws {
108+
// Given: A connection and a listener
109+
let mockConnection = MockConnection(state: .connecting)
110+
let mockRealtime = MockRealtime(connection: mockConnection)
111+
let connection = DefaultConnection(realtime: mockRealtime)
112+
let connectionError = ErrorInfo(
113+
code: 80003,
114+
message: "Connection lost",
115+
statusCode: 500,
116+
)
117+
118+
var receivedStatusChanges: [ConnectionStatusChange] = []
119+
120+
// When: Register a listener (CHA-CS4d)
121+
let subscription = connection.onStatusChange { statusChange in
122+
receivedStatusChanges.append(statusChange)
123+
}
124+
125+
// And: Emit a state change
126+
mockConnection.emit(.disconnected, event: .disconnected, error: connectionError)
127+
128+
// Then: The listener should receive the event with correct information
129+
#expect(receivedStatusChanges.count == 1)
130+
131+
let statusChange = receivedStatusChanges[0]
132+
// (CHA-CS4a) Contains newly entered connection status
133+
#expect(statusChange.current == .disconnected)
134+
// (CHA-CS4b) Contains previous connection status
135+
#expect(statusChange.previous == .connecting)
136+
// (CHA-CS4c) Contains connection error
137+
#expect(statusChange.error?.code == connectionError.code)
138+
#expect(statusChange.error?.message == connectionError.message)
139+
140+
// When: Unregister the listener (CHA-CS4e)
141+
subscription.off()
142+
receivedStatusChanges.removeAll()
143+
144+
// And: Emit a state change
145+
mockConnection.emit(.disconnected, event: .disconnected)
146+
147+
// Then: The listener should not receive any events
148+
#expect(receivedStatusChanges.isEmpty)
149+
}
150+
}

Tests/AblyChatTests/Mocks/MockConnection.swift

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,50 @@ import Ably
22
@testable import AblyChat
33

44
final class MockConnection: InternalConnectionProtocol {
5-
let state: ARTRealtimeConnectionState
5+
var state: ARTRealtimeConnectionState
66

7-
let errorReason: ErrorInfo?
7+
var errorReason: ErrorInfo?
8+
9+
private var listeners: [(ARTEventListener, @MainActor (ConnectionStateChange) -> Void)] = []
810

911
init(state: ARTRealtimeConnectionState = .initialized, errorReason: ErrorInfo? = nil) {
1012
self.state = state
1113
self.errorReason = errorReason
1214
}
1315

14-
func on(_: @escaping @MainActor (ConnectionStateChange) -> Void) -> ARTEventListener {
15-
fatalError("Not implemented")
16+
func on(_ callback: @escaping @MainActor (ConnectionStateChange) -> Void) -> ARTEventListener {
17+
let listener = ARTEventListener()
18+
listeners.append((listener, callback))
19+
return listener
20+
}
21+
22+
func off(_ listener: ARTEventListener) {
23+
listeners.removeAll { $0.0 === listener }
1624
}
1725

18-
func off(_: ARTEventListener) {
19-
fatalError("Not implemented")
26+
// Helper method to emit state changes for testing
27+
func emit(
28+
_ newState: ARTRealtimeConnectionState,
29+
event: ARTRealtimeConnectionEvent,
30+
error: ErrorInfo? = nil,
31+
retryIn: TimeInterval? = nil,
32+
) {
33+
let previousState = state
34+
state = newState
35+
if let error {
36+
errorReason = error
37+
}
38+
39+
let stateChange = ConnectionStateChange(
40+
current: newState,
41+
previous: previousState,
42+
event: event,
43+
reason: error,
44+
retryIn: retryIn ?? 0,
45+
)
46+
47+
for (_, callback) in listeners {
48+
callback(stateChange)
49+
}
2050
}
2151
}

0 commit comments

Comments
 (0)