@@ -11,6 +11,12 @@ import XCTest
1111@testable import Realtime
1212
1313final class ConnectionManagerTests : XCTestCase {
14+ private enum TestError : LocalizedError {
15+ case sample
16+
17+ var errorDescription : String ? { " sample error " }
18+ }
19+
1420 let url = URL ( string: " ws://localhost " ) !
1521 let headers = [ " apikey " : " key " ]
1622
@@ -38,10 +44,11 @@ final class ConnectionManagerTests: XCTestCase {
3844 private func makeSUT(
3945 url: URL = URL ( string: " ws://localhost " ) !,
4046 headers: [ String : String ] = [ : ] ,
41- reconnectDelay: TimeInterval = 0.1
47+ reconnectDelay: TimeInterval = 0.1 ,
48+ transport: WebSocketTransport ? = nil
4249 ) -> ConnectionManager {
4350 ConnectionManager (
44- transport: { url, headers in
51+ transport: transport ?? { url, headers in
4552 self . transportCallCount += 1
4653 self . lastConnectURL = url
4754 self . lastConnectHeaders = headers
@@ -54,24 +61,181 @@ final class ConnectionManagerTests: XCTestCase {
5461 )
5562 }
5663
57- func testConnect( ) async throws {
64+ func testConnectTransitionsThroughConnectingAndConnectedStates( ) async throws {
65+ sut = makeSUT ( url: url, headers: headers)
66+
67+ let connectingExpectation = expectation ( description: " connecting state observed " )
68+ let connectedExpectation = expectation ( description: " connected state observed " )
69+
70+ let stateObserver = Task {
71+ for await state in await sut. stateChanges {
72+ switch state {
73+ case . connecting:
74+ connectingExpectation. fulfill ( )
75+ case . connected:
76+ connectedExpectation. fulfill ( )
77+ return
78+ default :
79+ break
80+ }
81+ }
82+ }
83+
84+ let initiallyConnected = await sut. isConnected
85+ XCTAssertFalse ( initiallyConnected)
86+ try await sut. connect ( )
87+
88+ let isConnected = await sut. isConnected
89+ XCTAssertTrue ( isConnected)
90+ XCTAssertEqual ( transportCallCount, 1 )
91+ XCTAssertEqual ( lastConnectURL, url)
92+ XCTAssertEqual ( lastConnectHeaders, headers)
93+
94+ await fulfillment ( of: [ connectingExpectation, connectedExpectation] , timeout: 1 )
95+ stateObserver. cancel ( )
96+ }
97+
98+ func testConnectWhenAlreadyConnectedDoesNotReconnect( ) async throws {
5899 sut = makeSUT ( )
59100
60- let isConnectingExpectation = self . expectation ( description: " connecting state " )
101+ try await sut. connect ( )
102+ XCTAssertEqual ( transportCallCount, 1 )
103+
104+ try await sut. connect ( )
61105
62- Task {
63- _ = await sut. stateChanges. first { $0. isConnecting }
64- isConnectingExpectation. fulfill ( )
106+ let stillConnected = await sut. isConnected
107+ XCTAssertTrue ( stillConnected)
108+ XCTAssertEqual ( transportCallCount, 1 , " Second connect should reuse existing connection " )
109+ }
110+
111+ func testConnectWhileConnectingWaitsForExistingTask( ) async throws {
112+ sut = makeSUT (
113+ transport: { _, _ in
114+ self . transportCallCount += 1
115+ try await Task . sleep ( nanoseconds: 200_000_000 )
116+ return self . ws!
117+ }
118+ )
119+
120+ let firstConnect = Task {
121+ try await sut. connect ( )
122+ }
123+
124+ let secondConnectFinished = LockIsolated ( false )
125+ let secondConnect = Task {
126+ try await sut. connect ( )
127+ secondConnectFinished. setValue ( true )
65128 }
66129
67- var isConnected = await sut. isConnected
130+ try await Task . sleep ( nanoseconds: 50_000_000 )
131+ XCTAssertFalse ( secondConnectFinished. value)
132+ XCTAssertEqual (
133+ transportCallCount, 1 ,
134+ " Transport should be invoked only once while first connect is in progress " )
135+
136+ try await firstConnect. value
137+ try await secondConnect. value
138+
139+ XCTAssertTrue ( secondConnectFinished. value)
140+ let isConnected = await sut. isConnected
141+ XCTAssertTrue ( isConnected)
142+ XCTAssertEqual ( transportCallCount, 1 )
143+ }
144+
145+ func testDisconnectFromConnectedClosesWebSocketAndUpdatesState( ) async throws {
146+ sut = makeSUT ( )
147+ try await sut. connect ( )
148+
149+ await sut. disconnect ( reason: " test reason " )
150+
151+ let isConnected = await sut. isConnected
68152 XCTAssertFalse ( isConnected)
153+ guard case . close( let closeCode, let closeReason) ? = ws. sentEvents. last else {
154+ return XCTFail ( " Expected close event to be sent " )
155+ }
156+ XCTAssertNil ( closeCode)
157+ XCTAssertEqual ( closeReason, " test reason " )
158+ }
159+
160+ func testDisconnectCancelsOngoingConnectionAttempt( ) async throws {
161+ let wasCancelled = LockIsolated ( false )
162+
163+ sut = makeSUT (
164+ transport: { _, _ in
165+ self . transportCallCount += 1
166+ return try await withTaskCancellationHandler {
167+ try await Task . sleep ( nanoseconds: 5_000_000_000 )
168+ return self . ws!
169+ } onCancel: {
170+ wasCancelled. setValue ( true )
171+ }
172+ }
173+ )
174+
175+ let connectTask = Task {
176+ try ? await sut. connect ( )
177+ }
178+
179+ try await Task . sleep ( nanoseconds: 50_000_000 )
180+ await sut. disconnect ( reason: " stop " )
181+
182+ await Task . yield ( )
183+ XCTAssertTrue ( wasCancelled. value, " Cancellation handler should run when disconnecting " )
184+ let isConnected = await sut. isConnected
185+ XCTAssertFalse ( isConnected)
186+
187+ connectTask. cancel ( )
188+ }
189+
190+ func testHandleErrorInitiatesReconnectAndEventuallyReconnects( ) async throws {
191+ let reconnectingExpectation = expectation ( description: " reconnecting state observed " )
192+ let secondConnectionExpectation = expectation ( description: " second connection attempt " )
193+
194+ let connectionCount = LockIsolated ( 0 )
195+
196+ sut = makeSUT (
197+ reconnectDelay: 0.01 ,
198+ transport: { _, _ in
199+ connectionCount. withValue { $0 += 1 }
200+ if connectionCount. value == 2 {
201+ secondConnectionExpectation. fulfill ( )
202+ }
203+ return self . ws!
204+ }
205+ )
206+
207+ let stateObserver = Task {
208+ for await state in await sut. stateChanges {
209+ if case . reconnecting( _, let reason) = state, reason. contains ( " sample error " ) {
210+ reconnectingExpectation. fulfill ( )
211+ return
212+ }
213+ }
214+ }
215+
69216 try await sut. connect ( )
217+ await sut. handleError ( TestError . sample)
70218
71- isConnected = await sut. isConnected
219+ await fulfillment ( of: [ reconnectingExpectation, secondConnectionExpectation] , timeout: 2 )
220+ XCTAssertEqual ( connectionCount. value, 2 , " Reconnection should trigger a second transport call " )
221+ let isConnected = await sut. isConnected
72222 XCTAssertTrue ( isConnected)
73223
74- await fulfillment ( of : [ isConnectingExpectation ] , timeout : 1 )
224+ stateObserver . cancel ( )
75225 }
76226
227+ func testHandleCloseDelegatesToDisconnect( ) async throws {
228+ sut = makeSUT ( )
229+ try await sut. connect ( )
230+
231+ await sut. handleClose ( code: 4001 , reason: " server closing " )
232+
233+ let isConnected = await sut. isConnected
234+ XCTAssertFalse ( isConnected)
235+ guard case . close( let closeCode, let closeReason) ? = ws. sentEvents. last else {
236+ return XCTFail ( " Expected close event to be sent " )
237+ }
238+ XCTAssertNil ( closeCode)
239+ XCTAssertEqual ( closeReason, " server closing " )
240+ }
77241}
0 commit comments