@@ -11,6 +11,42 @@ private extension NSLock {
|
11 | 11 | } |
12 | 12 | } |
13 | 13 | |
| 14 | +private final class DoubleCallbackPingWebSocketTask: WebSocketTasking, @unchecked Sendable { |
| 15 | +private let callbacks: [Error?] |
| 16 | + |
| 17 | +init(callbacks: [Error?]) { |
| 18 | +self.callbacks = callbacks |
| 19 | +} |
| 20 | + |
| 21 | +var state: URLSessionTask.State { .running } |
| 22 | + |
| 23 | +func resume() {} |
| 24 | + |
| 25 | +func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { |
| 26 | + _ = (closeCode, reason) |
| 27 | +} |
| 28 | + |
| 29 | +func send(_ message: URLSessionWebSocketTask.Message) async throws { |
| 30 | + _ = message |
| 31 | +} |
| 32 | + |
| 33 | +func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) { |
| 34 | +for callback in self.callbacks { |
| 35 | +pongReceiveHandler(callback) |
| 36 | +} |
| 37 | +} |
| 38 | + |
| 39 | +func receive() async throws -> URLSessionWebSocketTask.Message { |
| 40 | +throw URLError(.badServerResponse) |
| 41 | +} |
| 42 | + |
| 43 | +func receive( |
| 44 | + completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void) |
| 45 | +{ |
| 46 | +completionHandler(.failure(URLError(.badServerResponse))) |
| 47 | +} |
| 48 | +} |
| 49 | + |
14 | 50 | private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Sendable { |
15 | 51 | private let lock = NSLock() |
16 | 52 | private let helloAuth: [String: Any]? |
@@ -193,6 +229,25 @@ private actor SeqGapProbe {
|
193 | 229 | |
194 | 230 | @Suite(.serialized) |
195 | 231 | struct GatewayNodeSessionTests { |
| 232 | +@Test |
| 233 | +func websocketPingIgnoresDuplicateSuccessCallbacks() async throws { |
| 234 | +let task = DoubleCallbackPingWebSocketTask(callbacks: [nil, nil]) |
| 235 | +try await WebSocketTaskBox(task: task).sendPing() |
| 236 | +} |
| 237 | + |
| 238 | +@Test |
| 239 | +func websocketPingIgnoresDuplicateCallbacksAfterFirstError() async throws { |
| 240 | +let firstError = URLError(.networkConnectionLost) |
| 241 | +let task = DoubleCallbackPingWebSocketTask(callbacks: [firstError, nil]) |
| 242 | + |
| 243 | +do { |
| 244 | +try await WebSocketTaskBox(task: task).sendPing() |
| 245 | +Issue.record("sendPing unexpectedly succeeded") |
| 246 | +} catch let error as URLError { |
| 247 | + #expect(error.code == firstError.code) |
| 248 | +} |
| 249 | +} |
| 250 | + |
196 | 251 | @Test |
197 | 252 | func scannedSetupCodePrefersBootstrapAuthOverStoredDeviceToken() async throws { |
198 | 253 | let tempDir = FileManager.default.temporaryDirectory |
|