Skip to content

Commit ee94a70

Browse files
committed
test(realtime): cover ConnectionManager
1 parent 0875296 commit ee94a70

File tree

1 file changed

+174
-10
lines changed

1 file changed

+174
-10
lines changed

Tests/RealtimeTests/ConnectionManagerTests.swift

Lines changed: 174 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import XCTest
1111
@testable import Realtime
1212

1313
final 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

Comments
 (0)