1+ import AsyncExtensions
12import Combine
3+ import NIO
4+ import NIOWebSocket
25import Synchronized
36@testable import WebSocket
47import XCTest
@@ -40,7 +43,7 @@ class SystemWebSocketTests: XCTestCase {
4043 onOpen: { XCTFail ( " Should not have opened " ) } ,
4144 onClose: { close in
4245 XCTAssertEqual ( . abnormalClosure, close. code)
43- XCTAssertNil ( close. reason)
46+ XCTAssertNotNil ( close. reason)
4447 ex. fulfill ( )
4548 }
4649 )
@@ -52,6 +55,52 @@ class SystemWebSocketTests: XCTestCase {
5255 XCTAssertTrue ( isClosed)
5356 }
5457
58+ func testOpenCancellationThrowsCancellationError( ) async throws {
59+ let server = try HangingServer ( )
60+ defer { server. shutDown ( ) }
61+
62+ let client = try await SystemWebSocket (
63+ request: request ( server. port) ,
64+ options: . init( timeoutIntervalForRequest: 5 )
65+ )
66+
67+ let openTask = Task {
68+ try await client. open ( )
69+ }
70+
71+ try await Task . sleep ( nanoseconds: 50 * NSEC_PER_MSEC)
72+ openTask. cancel ( )
73+
74+ switch await openTask. result {
75+ case . success:
76+ XCTFail ( " Expected `open()` to throw `CancellationError` " )
77+
78+ case let . failure( error) :
79+ XCTAssertTrue (
80+ error is CancellationError ,
81+ " Received wrong error: \( String ( reflecting: error) ) "
82+ )
83+ }
84+ }
85+
86+ func testOpenThrowsConnectionErrorWhenServerIsUnreachable( ) async throws {
87+ let ( server, client) = try await makeOfflineServerAndClient (
88+ timeoutIntervalForRequest: 0.2
89+ )
90+ defer { server. shutDown ( ) }
91+
92+ do {
93+ try await client. open ( )
94+ XCTFail ( " Should not have opened " )
95+ } catch is TimeoutError {
96+ XCTFail ( " Should surface the connection failure instead of timing out " )
97+ } catch let error as WebSocketError {
98+ XCTAssertEqual ( . abnormalClosure, error. closeCode)
99+ } catch {
100+ XCTFail ( " Received wrong error: \( error) " )
101+ }
102+ }
103+
55104 func _testErrorWhenRemoteCloses( ) async throws {
56105 let errorEx = expectation ( description: " Should have closed " )
57106 let ( server, client) = try await makeServerAndClient (
@@ -114,6 +163,53 @@ class SystemWebSocketTests: XCTestCase {
114163 await fulfillment ( of: [ secondCloseEx] , timeout: 0.1 )
115164 }
116165
166+ func testDelegateDoesNotReorderOpenAndCloseCallbacks( ) async throws {
167+ let delegate = Delegate ( )
168+ let session = URLSession ( configuration: . ephemeral)
169+ defer { session. invalidateAndCancel ( ) }
170+
171+ let task = session. webSocketTask ( with: URL ( string: " ws://127.0.0.1/socket " ) !)
172+ let openStarted = AsyncThrowingFuture < Void > ( timeout: 2 )
173+ let allowOpenToFinish = AsyncThrowingFuture < Void > ( timeout: 2 )
174+ let records = Locked ( [ String] ( ) )
175+
176+ delegate. set (
177+ onOpen: {
178+ records. access { $0. append ( " open-started " ) }
179+ openStarted. resolve ( )
180+ do { try await allowOpenToFinish. value }
181+ catch { XCTFail ( ) }
182+ records. access { $0. append ( " open-finished " ) }
183+ } ,
184+ onClose: { _, _ in
185+ records. access { $0. append ( " close " ) }
186+ } ,
187+ for: ObjectIdentifier ( task)
188+ )
189+
190+ delegate. urlSession ( session, webSocketTask: task, didOpenWithProtocol: nil )
191+ try await openStarted. value
192+
193+ delegate. urlSession (
194+ session,
195+ webSocketTask: task,
196+ didCloseWith: . goingAway,
197+ reason: nil
198+ )
199+
200+ try await Task . sleep ( nanoseconds: 10 * NSEC_PER_MSEC)
201+ let eventsBeforeOpenFinishes = records. access { $0 }
202+ XCTAssertEqual ( [ " open-started " ] , eventsBeforeOpenFinishes)
203+
204+ allowOpenToFinish. resolve ( )
205+ try await Task . sleep ( nanoseconds: 10 * NSEC_PER_MSEC)
206+ let eventsAfterOpenFinishes = records. access { $0 }
207+ XCTAssertEqual (
208+ [ " open-started " , " open-finished " , " close " ] ,
209+ eventsAfterOpenFinishes
210+ )
211+ }
212+
117213 func testPushAndReceiveText( ) async throws {
118214 let ( server, client) = try await makeServerAndClient ( )
119215 defer { server. shutDown ( ) }
@@ -338,9 +434,27 @@ class SystemWebSocketTests: XCTestCase {
338434 }
339435 }
340436
437+ await fulfillment ( of: [ closeEx] , timeout: 2 )
438+
341439 XCTAssertEqual ( 3 , messagesReceivedByClient)
342440 XCTAssertEqual ( 3 , messagesReceivedByServer)
441+ }
343442
443+ func testRemoteCloseReasonIsPassedToOnClose( ) async throws {
444+ let closeEx = expectation ( description: " Should expose the close reason " )
445+ let reason = Data ( " server said goodbye " . utf8)
446+
447+ let ( server, client) = try await makeServerAndClient (
448+ onClose: { close in
449+ XCTAssertEqual ( . goingAway, close. code)
450+ XCTAssertEqual ( reason, close. reason)
451+ closeEx. fulfill ( )
452+ }
453+ )
454+ defer { server. shutDown ( ) }
455+
456+ try await client. open ( )
457+ subject. send ( . remoteCloseWithReason( . goingAway, reason) )
344458 await fulfillment ( of: [ closeEx] , timeout: 2 )
345459 }
346460}
@@ -373,13 +487,14 @@ private extension SystemWebSocketTests {
373487 }
374488
375489 func makeOfflineServerAndClient(
490+ timeoutIntervalForRequest: TimeInterval = 2 ,
376491 onOpen: @escaping @Sendable ( ) -> Void = { } ,
377492 onClose: @escaping @Sendable ( WebSocketClose ) -> Void = { _ in }
378493 ) async throws -> ( WebSocketServer , SystemWebSocket ) {
379494 let server = try WebSocketServer ( outputPublisher: empty)
380495 let client = try ! await SystemWebSocket (
381496 request: request ( 19 ) ,
382- options: . init( timeoutIntervalForRequest: 2 ) ,
497+ options: . init( timeoutIntervalForRequest: timeoutIntervalForRequest ) ,
383498 onOpen: onOpen,
384499 onClose: onClose
385500 )
@@ -400,3 +515,28 @@ private extension SystemWebSocketTests {
400515 return ( server, try ! await . system( client) )
401516 }
402517}
518+
519+ private final class HangingServer {
520+ var port : Int { channel!. localAddress!. port! }
521+
522+ private let eventLoopGroup : EventLoopGroup
523+ private var channel : Channel ?
524+
525+ init ( ) throws {
526+ eventLoopGroup = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
527+ channel = try ServerBootstrap ( group: eventLoopGroup)
528+ . serverChannelOption ( ChannelOptions . backlog, value: 256 )
529+ . serverChannelOption ( ChannelOptions . socketOption ( . so_reuseaddr) , value: 1 )
530+ . childChannelInitializer { channel in
531+ channel. eventLoop. makeSucceededFuture ( ( ) )
532+ }
533+ . childChannelOption ( ChannelOptions . socketOption ( . so_reuseaddr) , value: 1 )
534+ . bind ( host: " 127.0.0.1 " , port: 0 )
535+ . wait ( )
536+ }
537+
538+ func shutDown( ) {
539+ try ? channel? . close ( mode: . all) . wait ( )
540+ try ? eventLoopGroup. syncShutdownGracefully ( )
541+ }
542+ }
0 commit comments