mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Improve iOS tests
This commit is contained in:
parent
a791bbede8
commit
a121d09fee
6 changed files with 657 additions and 317 deletions
|
|
@ -49,8 +49,8 @@ class BufferWebSocketClient: NSObject {
|
||||||
/// Magic byte for binary messages
|
/// Magic byte for binary messages
|
||||||
private static let bufferMagicByte: UInt8 = 0xBF
|
private static let bufferMagicByte: UInt8 = 0xBF
|
||||||
|
|
||||||
private var webSocketTask: URLSessionWebSocketTask?
|
private var webSocket: WebSocketProtocol?
|
||||||
private let session = URLSession(configuration: .default)
|
private let webSocketFactory: WebSocketFactory
|
||||||
private var subscriptions = [String: (TerminalWebSocketEvent) -> Void]()
|
private var subscriptions = [String: (TerminalWebSocketEvent) -> Void]()
|
||||||
private var reconnectTask: Task<Void, Never>?
|
private var reconnectTask: Task<Void, Never>?
|
||||||
private var reconnectAttempts = 0
|
private var reconnectAttempts = 0
|
||||||
|
|
@ -70,6 +70,11 @@ class BufferWebSocketClient: NSObject {
|
||||||
return serverConfig.baseURL
|
return serverConfig.baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(webSocketFactory: WebSocketFactory = DefaultWebSocketFactory()) {
|
||||||
|
self.webSocketFactory = webSocketFactory
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
func connect() {
|
func connect() {
|
||||||
guard !isConnecting else { return }
|
guard !isConnecting else { return }
|
||||||
guard let baseURL else {
|
guard let baseURL else {
|
||||||
|
|
@ -93,39 +98,27 @@ class BufferWebSocketClient: NSObject {
|
||||||
|
|
||||||
print("[BufferWebSocket] Connecting to \(wsURL)")
|
print("[BufferWebSocket] Connecting to \(wsURL)")
|
||||||
|
|
||||||
// Cancel existing task if any
|
// Disconnect existing WebSocket if any
|
||||||
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
webSocket?.disconnect(with: .goingAway, reason: nil)
|
||||||
|
|
||||||
// Create request with authentication
|
// Create new WebSocket
|
||||||
var request = URLRequest(url: wsURL)
|
webSocket = webSocketFactory.createWebSocket()
|
||||||
|
webSocket?.delegate = self
|
||||||
|
|
||||||
|
// Build headers
|
||||||
|
var headers: [String: String] = [:]
|
||||||
|
|
||||||
// Add authentication header if needed
|
// Add authentication header if needed
|
||||||
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"),
|
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"),
|
||||||
let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config),
|
let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config),
|
||||||
let authHeader = serverConfig.authorizationHeader {
|
let authHeader = serverConfig.authorizationHeader {
|
||||||
request.setValue(authHeader, forHTTPHeaderField: "Authorization")
|
headers["Authorization"] = authHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new WebSocket task
|
// Connect
|
||||||
webSocketTask = session.webSocketTask(with: request)
|
|
||||||
webSocketTask?.resume()
|
|
||||||
|
|
||||||
// Start receiving messages
|
|
||||||
receiveMessage()
|
|
||||||
|
|
||||||
// Send initial ping to establish connection
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await sendPing()
|
try await webSocket?.connect(to: wsURL, with: headers)
|
||||||
isConnected = true
|
|
||||||
isConnecting = false
|
|
||||||
reconnectAttempts = 0
|
|
||||||
startPingTask()
|
|
||||||
|
|
||||||
// Re-subscribe to all sessions
|
|
||||||
for sessionId in subscriptions.keys {
|
|
||||||
try await subscribe(to: sessionId)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
print("[BufferWebSocket] Connection failed: \(error)")
|
print("[BufferWebSocket] Connection failed: \(error)")
|
||||||
connectionError = error
|
connectionError = error
|
||||||
|
|
@ -135,36 +128,13 @@ class BufferWebSocketClient: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func receiveMessage() {
|
private func handleMessage(_ message: WebSocketMessage) {
|
||||||
webSocketTask?.receive { [weak self] result in
|
|
||||||
guard let self else { return }
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success(let message):
|
|
||||||
Task { @MainActor in
|
|
||||||
self.handleMessage(message)
|
|
||||||
self.receiveMessage() // Continue receiving
|
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
print("[BufferWebSocket] Receive error: \(error)")
|
|
||||||
Task { @MainActor in
|
|
||||||
self.handleDisconnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleMessage(_ message: URLSessionWebSocketTask.Message) {
|
|
||||||
switch message {
|
switch message {
|
||||||
case .data(let data):
|
case .data(let data):
|
||||||
handleBinaryMessage(data)
|
handleBinaryMessage(data)
|
||||||
|
|
||||||
case .string(let text):
|
case .string(let text):
|
||||||
handleTextMessage(text)
|
handleTextMessage(text)
|
||||||
|
|
||||||
@unknown default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -632,7 +602,7 @@ class BufferWebSocketClient: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendMessage(_ message: [String: Any]) async throws {
|
private func sendMessage(_ message: [String: Any]) async throws {
|
||||||
guard let webSocketTask else {
|
guard let webSocket else {
|
||||||
throw WebSocketError.connectionFailed
|
throw WebSocketError.connectionFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -641,11 +611,14 @@ class BufferWebSocketClient: NSObject {
|
||||||
throw WebSocketError.invalidData
|
throw WebSocketError.invalidData
|
||||||
}
|
}
|
||||||
|
|
||||||
try await webSocketTask.send(.string(string))
|
try await webSocket.send(.string(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendPing() async throws {
|
private func sendPing() async throws {
|
||||||
try await sendMessage(["type": "ping"])
|
guard let webSocket else {
|
||||||
|
throw WebSocketError.connectionFailed
|
||||||
|
}
|
||||||
|
try await webSocket.sendPing()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startPingTask() {
|
private func startPingTask() {
|
||||||
|
|
@ -668,7 +641,7 @@ class BufferWebSocketClient: NSObject {
|
||||||
|
|
||||||
private func handleDisconnection() {
|
private func handleDisconnection() {
|
||||||
isConnected = false
|
isConnected = false
|
||||||
webSocketTask = nil
|
webSocket = nil
|
||||||
stopPingTask()
|
stopPingTask()
|
||||||
scheduleReconnect()
|
scheduleReconnect()
|
||||||
}
|
}
|
||||||
|
|
@ -697,8 +670,8 @@ class BufferWebSocketClient: NSObject {
|
||||||
reconnectTask = nil
|
reconnectTask = nil
|
||||||
stopPingTask()
|
stopPingTask()
|
||||||
|
|
||||||
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
webSocket?.disconnect(with: .goingAway, reason: nil)
|
||||||
webSocketTask = nil
|
webSocket = nil
|
||||||
|
|
||||||
subscriptions.removeAll()
|
subscriptions.removeAll()
|
||||||
isConnected = false
|
isConnected = false
|
||||||
|
|
@ -706,6 +679,40 @@ class BufferWebSocketClient: NSObject {
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
// Tasks will be cancelled automatically when the object is deallocated
|
// Tasks will be cancelled automatically when the object is deallocated
|
||||||
// WebSocket task cleanup happens in disconnect()
|
// WebSocket cleanup happens in disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - WebSocketDelegate
|
||||||
|
|
||||||
|
extension BufferWebSocketClient: WebSocketDelegate {
|
||||||
|
func webSocketDidConnect(_ webSocket: WebSocketProtocol) {
|
||||||
|
print("[BufferWebSocket] Connected")
|
||||||
|
isConnected = true
|
||||||
|
isConnecting = false
|
||||||
|
reconnectAttempts = 0
|
||||||
|
startPingTask()
|
||||||
|
|
||||||
|
// Re-subscribe to all sessions
|
||||||
|
Task {
|
||||||
|
for sessionId in subscriptions.keys {
|
||||||
|
try? await subscribe(to: sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func webSocket(_ webSocket: WebSocketProtocol, didReceiveMessage message: WebSocketMessage) {
|
||||||
|
handleMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func webSocket(_ webSocket: WebSocketProtocol, didFailWithError error: Error) {
|
||||||
|
print("[BufferWebSocket] Error: \(error)")
|
||||||
|
connectionError = error
|
||||||
|
handleDisconnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
func webSocketDidDisconnect(_ webSocket: WebSocketProtocol, closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||||
|
print("[BufferWebSocket] Disconnected with code: \(closeCode)")
|
||||||
|
handleDisconnection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
ios/VibeTunnel/Services/WebSocketFactory.swift
Normal file
15
ios/VibeTunnel/Services/WebSocketFactory.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Protocol for creating WebSocket instances
|
||||||
|
@MainActor
|
||||||
|
protocol WebSocketFactory {
|
||||||
|
func createWebSocket() -> WebSocketProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default factory that creates real WebSocket instances
|
||||||
|
@MainActor
|
||||||
|
class DefaultWebSocketFactory: WebSocketFactory {
|
||||||
|
func createWebSocket() -> WebSocketProtocol {
|
||||||
|
return URLSessionWebSocket()
|
||||||
|
}
|
||||||
|
}
|
||||||
152
ios/VibeTunnel/Services/WebSocketProtocol.swift
Normal file
152
ios/VibeTunnel/Services/WebSocketProtocol.swift
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Protocol for WebSocket operations to enable testing
|
||||||
|
@MainActor
|
||||||
|
protocol WebSocketProtocol: AnyObject {
|
||||||
|
var delegate: WebSocketDelegate? { get set }
|
||||||
|
|
||||||
|
func connect(to url: URL, with headers: [String: String]) async throws
|
||||||
|
func send(_ message: WebSocketMessage) async throws
|
||||||
|
func sendPing() async throws
|
||||||
|
func disconnect(with code: URLSessionWebSocketTask.CloseCode, reason: Data?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket message types
|
||||||
|
enum WebSocketMessage {
|
||||||
|
case string(String)
|
||||||
|
case data(Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delegate protocol for WebSocket events
|
||||||
|
@MainActor
|
||||||
|
protocol WebSocketDelegate: AnyObject {
|
||||||
|
func webSocketDidConnect(_ webSocket: WebSocketProtocol)
|
||||||
|
func webSocket(_ webSocket: WebSocketProtocol, didReceiveMessage message: WebSocketMessage)
|
||||||
|
func webSocket(_ webSocket: WebSocketProtocol, didFailWithError error: Error)
|
||||||
|
func webSocketDidDisconnect(_ webSocket: WebSocketProtocol, closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Real implementation of WebSocketProtocol using URLSessionWebSocketTask
|
||||||
|
@MainActor
|
||||||
|
class URLSessionWebSocket: NSObject, WebSocketProtocol {
|
||||||
|
weak var delegate: WebSocketDelegate?
|
||||||
|
private var webSocketTask: URLSessionWebSocketTask?
|
||||||
|
private var session: URLSession!
|
||||||
|
private var isReceiving = false
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(to url: URL, with headers: [String: String]) async throws {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||||
|
|
||||||
|
webSocketTask = session.webSocketTask(with: request)
|
||||||
|
webSocketTask?.resume()
|
||||||
|
|
||||||
|
// Start receiving messages
|
||||||
|
isReceiving = true
|
||||||
|
receiveNextMessage()
|
||||||
|
|
||||||
|
// Send initial ping to verify connection
|
||||||
|
do {
|
||||||
|
try await sendPing()
|
||||||
|
Task { @MainActor in
|
||||||
|
self.delegate?.webSocketDidConnect(self)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Task { @MainActor in
|
||||||
|
self.delegate?.webSocket(self, didFailWithError: error)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(_ message: WebSocketMessage) async throws {
|
||||||
|
guard let task = webSocketTask else {
|
||||||
|
throw WebSocketError.connectionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
switch message {
|
||||||
|
case .string(let text):
|
||||||
|
try await task.send(.string(text))
|
||||||
|
case .data(let data):
|
||||||
|
try await task.send(.data(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPing() async throws {
|
||||||
|
guard let task = webSocketTask else {
|
||||||
|
throw WebSocketError.connectionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
task.sendPing { error in
|
||||||
|
if let error = error {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
} else {
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(with code: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||||
|
isReceiving = false
|
||||||
|
webSocketTask?.cancel(with: code, reason: reason)
|
||||||
|
Task { @MainActor in
|
||||||
|
self.delegate?.webSocketDidDisconnect(self, closeCode: code, reason: reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func receiveNextMessage() {
|
||||||
|
guard isReceiving, let task = webSocketTask else { return }
|
||||||
|
|
||||||
|
task.receive { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let message):
|
||||||
|
let wsMessage: WebSocketMessage
|
||||||
|
switch message {
|
||||||
|
case .string(let text):
|
||||||
|
wsMessage = .string(text)
|
||||||
|
case .data(let data):
|
||||||
|
wsMessage = .data(data)
|
||||||
|
@unknown default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
self.delegate?.webSocket(self, didReceiveMessage: wsMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue receiving
|
||||||
|
Task { @MainActor in
|
||||||
|
self.receiveNextMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
Task { @MainActor in
|
||||||
|
self.isReceiving = false
|
||||||
|
self.delegate?.webSocket(self, didFailWithError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension URLSessionWebSocket: URLSessionWebSocketDelegate {
|
||||||
|
nonisolated func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
|
||||||
|
// Connection opened - already handled in connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||||
|
Task { @MainActor in
|
||||||
|
self.isReceiving = false
|
||||||
|
self.delegate?.webSocketDidDisconnect(self, closeCode: closeCode, reason: reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
196
ios/VibeTunnelTests/Mocks/MockWebSocket.swift
Normal file
196
ios/VibeTunnelTests/Mocks/MockWebSocket.swift
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
import Foundation
|
||||||
|
@testable import VibeTunnel
|
||||||
|
|
||||||
|
/// Mock WebSocket implementation for testing
|
||||||
|
@MainActor
|
||||||
|
class MockWebSocket: WebSocketProtocol {
|
||||||
|
weak var delegate: WebSocketDelegate?
|
||||||
|
|
||||||
|
// State tracking
|
||||||
|
private(set) var isConnected = false
|
||||||
|
private(set) var lastConnectURL: URL?
|
||||||
|
private(set) var lastConnectHeaders: [String: String]?
|
||||||
|
private(set) var sentMessages: [WebSocketMessage] = []
|
||||||
|
private(set) var pingCount = 0
|
||||||
|
private(set) var disconnectCalled = false
|
||||||
|
private(set) var lastDisconnectCode: URLSessionWebSocketTask.CloseCode?
|
||||||
|
private(set) var lastDisconnectReason: Data?
|
||||||
|
|
||||||
|
// Control test behavior
|
||||||
|
var shouldFailConnection = false
|
||||||
|
var connectionError: Error?
|
||||||
|
var shouldFailSend = false
|
||||||
|
var sendError: Error?
|
||||||
|
var shouldFailPing = false
|
||||||
|
var pingError: Error?
|
||||||
|
|
||||||
|
// Message simulation
|
||||||
|
private var messageQueue: [WebSocketMessage] = []
|
||||||
|
private var messageDeliveryTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
func connect(to url: URL, with headers: [String: String]) async throws {
|
||||||
|
lastConnectURL = url
|
||||||
|
lastConnectHeaders = headers
|
||||||
|
|
||||||
|
if shouldFailConnection {
|
||||||
|
let error = connectionError ?? WebSocketError.connectionFailed
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected = true
|
||||||
|
delegate?.webSocketDidConnect(self)
|
||||||
|
|
||||||
|
// Start delivering queued messages
|
||||||
|
startMessageDelivery()
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(_ message: WebSocketMessage) async throws {
|
||||||
|
guard isConnected else {
|
||||||
|
throw WebSocketError.connectionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldFailSend {
|
||||||
|
throw sendError ?? WebSocketError.connectionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
sentMessages.append(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPing() async throws {
|
||||||
|
guard isConnected else {
|
||||||
|
throw WebSocketError.connectionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldFailPing {
|
||||||
|
throw pingError ?? WebSocketError.connectionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
pingCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(with code: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||||
|
disconnectCalled = true
|
||||||
|
lastDisconnectCode = code
|
||||||
|
lastDisconnectReason = reason
|
||||||
|
|
||||||
|
if isConnected {
|
||||||
|
isConnected = false
|
||||||
|
messageDeliveryTask?.cancel()
|
||||||
|
messageDeliveryTask = nil
|
||||||
|
delegate?.webSocketDidDisconnect(self, closeCode: code, reason: reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Helpers
|
||||||
|
|
||||||
|
/// Simulate receiving a message from the server
|
||||||
|
func simulateMessage(_ message: WebSocketMessage) {
|
||||||
|
guard isConnected else { return }
|
||||||
|
messageQueue.append(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate multiple messages
|
||||||
|
func simulateMessages(_ messages: [WebSocketMessage]) {
|
||||||
|
guard isConnected else { return }
|
||||||
|
messageQueue.append(contentsOf: messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a connection error
|
||||||
|
func simulateError(_ error: Error) {
|
||||||
|
guard isConnected else { return }
|
||||||
|
delegate?.webSocket(self, didFailWithError: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate server disconnection
|
||||||
|
func simulateDisconnection(closeCode: URLSessionWebSocketTask.CloseCode = .abnormalClosure, reason: Data? = nil) {
|
||||||
|
guard isConnected else { return }
|
||||||
|
isConnected = false
|
||||||
|
messageDeliveryTask?.cancel()
|
||||||
|
messageDeliveryTask = nil
|
||||||
|
delegate?.webSocketDidDisconnect(self, closeCode: closeCode, reason: reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all tracked state
|
||||||
|
func reset() {
|
||||||
|
isConnected = false
|
||||||
|
lastConnectURL = nil
|
||||||
|
lastConnectHeaders = nil
|
||||||
|
sentMessages.removeAll()
|
||||||
|
pingCount = 0
|
||||||
|
disconnectCalled = false
|
||||||
|
lastDisconnectCode = nil
|
||||||
|
lastDisconnectReason = nil
|
||||||
|
messageQueue.removeAll()
|
||||||
|
messageDeliveryTask?.cancel()
|
||||||
|
messageDeliveryTask = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find sent messages by type
|
||||||
|
func sentStringMessages() -> [String] {
|
||||||
|
sentMessages.compactMap { message in
|
||||||
|
if case .string(let text) = message {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sentDataMessages() -> [Data] {
|
||||||
|
sentMessages.compactMap { message in
|
||||||
|
if case .data(let data) = message {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find sent JSON messages
|
||||||
|
func sentJSONMessages() -> [[String: Any]] {
|
||||||
|
sentStringMessages().compactMap { string in
|
||||||
|
guard let data = string.data(using: .utf8),
|
||||||
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startMessageDelivery() {
|
||||||
|
messageDeliveryTask = Task { [weak self] in
|
||||||
|
while !Task.isCancelled {
|
||||||
|
guard let self = self else { break }
|
||||||
|
|
||||||
|
if !messageQueue.isEmpty {
|
||||||
|
let message = messageQueue.removeFirst()
|
||||||
|
await MainActor.run {
|
||||||
|
self.delegate?.webSocket(self, didReceiveMessage: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to simulate network latency
|
||||||
|
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mock WebSocket factory for testing
|
||||||
|
@MainActor
|
||||||
|
class MockWebSocketFactory: WebSocketFactory {
|
||||||
|
private(set) var createdWebSockets: [MockWebSocket] = []
|
||||||
|
|
||||||
|
func createWebSocket() -> WebSocketProtocol {
|
||||||
|
let webSocket = MockWebSocket()
|
||||||
|
createdWebSockets.append(webSocket)
|
||||||
|
return webSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastCreatedWebSocket: MockWebSocket? {
|
||||||
|
createdWebSockets.last
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
createdWebSockets.forEach { $0.reset() }
|
||||||
|
createdWebSockets.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,45 +1,3 @@
|
||||||
|
// This file is kept for Xcode project compatibility
|
||||||
|
// The actual mock implementation is in MockWebSocket.swift
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Simple mock WebSocket session for testing
|
|
||||||
/// Note: This is a placeholder implementation since we can't easily mock URLSessionWebSocketTask
|
|
||||||
/// Real tests should use dependency injection or network stubbing libraries
|
|
||||||
class MockWebSocketSession {
|
|
||||||
var mockTask: Any?
|
|
||||||
var lastURL: URL?
|
|
||||||
var lastRequest: URLRequest?
|
|
||||||
|
|
||||||
func webSocketTask(with url: URL) -> Any {
|
|
||||||
lastURL = url
|
|
||||||
return NSObject() // Return a dummy object
|
|
||||||
}
|
|
||||||
|
|
||||||
func webSocketTask(with request: URLRequest) -> Any {
|
|
||||||
lastRequest = request
|
|
||||||
return NSObject() // Return a dummy object
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Placeholder for future WebSocket testing implementation
|
|
||||||
/// Currently, WebSocket tests are limited to conceptual testing
|
|
||||||
/// due to URLSessionWebSocketTask not being easily mockable
|
|
||||||
struct WebSocketTestHelper {
|
|
||||||
static func createMockBinaryMessage(cols: Int32, rows: Int32) -> Data {
|
|
||||||
var data = Data()
|
|
||||||
|
|
||||||
// Magic byte
|
|
||||||
data.append(0xBF)
|
|
||||||
|
|
||||||
// Header (5 Int32 values in little endian)
|
|
||||||
let viewportY: Int32 = 0
|
|
||||||
let cursorX: Int32 = 0
|
|
||||||
let cursorY: Int32 = 0
|
|
||||||
|
|
||||||
data.append(contentsOf: withUnsafeBytes(of: cols.littleEndian) { Array($0) })
|
|
||||||
data.append(contentsOf: withUnsafeBytes(of: rows.littleEndian) { Array($0) })
|
|
||||||
data.append(contentsOf: withUnsafeBytes(of: viewportY.littleEndian) { Array($0) })
|
|
||||||
data.append(contentsOf: withUnsafeBytes(of: cursorX.littleEndian) { Array($0) })
|
|
||||||
data.append(contentsOf: withUnsafeBytes(of: cursorY.littleEndian) { Array($0) })
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,262 +8,274 @@ struct BufferWebSocketClientTests {
|
||||||
@Test("Connects successfully with valid configuration")
|
@Test("Connects successfully with valid configuration")
|
||||||
func successfulConnection() async throws {
|
func successfulConnection() async throws {
|
||||||
// Arrange
|
// Arrange
|
||||||
let client = BufferWebSocketClient()
|
let mockFactory = MockWebSocketFactory()
|
||||||
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
saveTestServerConfig()
|
saveTestServerConfig()
|
||||||
|
|
||||||
// Note: This test would require modifying BufferWebSocketClient to accept a custom URLSession
|
|
||||||
// For now, we'll test the connection logic conceptually
|
|
||||||
// let mockSession = MockWebSocketSession()
|
|
||||||
// let mockTask = mockSession.webSocketTask(with: URL(string: "ws://localhost:8888")!)
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
|
// Give it a moment to process
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// In a real test, we'd verify the WebSocket connection was established
|
#expect(mockFactory.createdWebSockets.count == 1)
|
||||||
// For now, we verify the client doesn't crash and sets appropriate state
|
let mockWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
|
#expect(mockWebSocket?.isConnected == true)
|
||||||
|
#expect(mockWebSocket?.lastConnectURL?.absoluteString.contains("/buffers") == true)
|
||||||
|
#expect(client.isConnected == true)
|
||||||
#expect(client.connectionError == nil)
|
#expect(client.connectionError == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Handles connection failure gracefully")
|
@Test("Handles connection failure gracefully")
|
||||||
func connectionFailure() async throws {
|
func connectionFailure() async throws {
|
||||||
// Arrange
|
// Arrange
|
||||||
let client = BufferWebSocketClient()
|
let mockFactory = MockWebSocketFactory()
|
||||||
// Don't save server config to trigger connection failure
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
UserDefaults.standard.removeObject(forKey: "savedServerConfig")
|
saveTestServerConfig()
|
||||||
|
|
||||||
// Act
|
// Configure mock to fail
|
||||||
client.connect()
|
client.connect()
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
||||||
|
|
||||||
|
let mockWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
|
mockWebSocket?.shouldFailConnection = true
|
||||||
|
mockWebSocket?.connectionError = WebSocketError.connectionFailed
|
||||||
|
|
||||||
|
// Simulate connection failure
|
||||||
|
mockWebSocket?.simulateError(WebSocketError.connectionFailed)
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
#expect(client.connectionError != nil)
|
|
||||||
#expect(client.isConnected == false)
|
#expect(client.isConnected == false)
|
||||||
|
#expect(client.connectionError != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Parses binary buffer messages correctly")
|
@Test("Parses binary buffer messages correctly")
|
||||||
func binaryMessageParsing() async throws {
|
func binaryMessageParsing() async throws {
|
||||||
// Arrange
|
// Arrange
|
||||||
let client = BufferWebSocketClient()
|
let mockFactory = MockWebSocketFactory()
|
||||||
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
|
saveTestServerConfig()
|
||||||
|
|
||||||
var receivedEvent: TerminalWebSocketEvent?
|
var receivedEvent: TerminalWebSocketEvent?
|
||||||
|
let sessionId = "test-session-123"
|
||||||
|
|
||||||
// Subscribe to events
|
// Subscribe to events
|
||||||
client.subscribe(id: "test") { event in
|
client.subscribe(to: sessionId) { event in
|
||||||
receivedEvent = event
|
receivedEvent = event
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a mock binary message
|
// Connect
|
||||||
let bufferData = TestFixtures.bufferSnapshot(cols: 80, rows: 24)
|
client.connect()
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
// Act - Simulate receiving a binary message
|
let mockWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
// This would normally come through the WebSocket
|
#expect(mockWebSocket?.isConnected == true)
|
||||||
// We'd need to expose a method for testing or use dependency injection
|
|
||||||
|
|
||||||
// For demonstration, let's test the parsing logic conceptually
|
// Create a binary message with proper structure
|
||||||
#expect(bufferData.first == 0xBF) // Magic byte
|
var messageData = Data()
|
||||||
|
|
||||||
// Verify data structure
|
// Magic byte for buffer message
|
||||||
var offset = 1
|
messageData.append(0xBF)
|
||||||
let cols = bufferData.withUnsafeBytes { bytes in
|
|
||||||
bytes.load(fromByteOffset: offset, as: Int32.self).littleEndian
|
// Session ID length (4 bytes, little endian)
|
||||||
|
let sessionIdData = sessionId.data(using: .utf8)!
|
||||||
|
var sessionIdLength = UInt32(sessionIdData.count).littleEndian
|
||||||
|
messageData.append(Data(bytes: &sessionIdLength, count: 4))
|
||||||
|
|
||||||
|
// Session ID
|
||||||
|
messageData.append(sessionIdData)
|
||||||
|
|
||||||
|
// Buffer data with header
|
||||||
|
messageData.append(TestFixtures.bufferSnapshot(cols: 80, rows: 24))
|
||||||
|
|
||||||
|
// Act - Simulate receiving the message
|
||||||
|
mockWebSocket?.simulateMessage(.data(messageData))
|
||||||
|
|
||||||
|
// Wait for processing
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
#expect(receivedEvent != nil)
|
||||||
|
if case .bufferUpdate(let snapshot) = receivedEvent {
|
||||||
|
#expect(snapshot.cols == 80)
|
||||||
|
#expect(snapshot.rows == 24)
|
||||||
|
} else {
|
||||||
|
Issue.record("Expected buffer update event")
|
||||||
}
|
}
|
||||||
offset += 4
|
|
||||||
|
|
||||||
let rows = bufferData.withUnsafeBytes { bytes in
|
|
||||||
bytes.load(fromByteOffset: offset, as: Int32.self).littleEndian
|
|
||||||
}
|
|
||||||
|
|
||||||
#expect(cols == 80)
|
|
||||||
#expect(rows == 24)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Handles text messages for events")
|
@Test("Handles text messages for events")
|
||||||
func textMessageHandling() async throws {
|
func textMessageHandling() async throws {
|
||||||
// Arrange
|
// Arrange
|
||||||
let client = BufferWebSocketClient()
|
let mockFactory = MockWebSocketFactory()
|
||||||
var receivedEvents: [TerminalWebSocketEvent] = []
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
|
|
||||||
client.subscribe(id: "test") { event in
|
|
||||||
receivedEvents.append(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test various text message formats
|
|
||||||
let messages = [
|
|
||||||
"""
|
|
||||||
{"type":"exit","code":0}
|
|
||||||
""",
|
|
||||||
"""
|
|
||||||
{"type":"bell"}
|
|
||||||
""",
|
|
||||||
"""
|
|
||||||
{"type":"alert","title":"Warning","message":"Session timeout"}
|
|
||||||
"""
|
|
||||||
]
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
// In a real implementation, we'd send these through the WebSocket
|
|
||||||
// and verify the correct events are generated
|
|
||||||
|
|
||||||
// Verify JSON structure
|
|
||||||
for message in messages {
|
|
||||||
let data = message.data(using: .utf8)!
|
|
||||||
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
||||||
#expect(json != nil)
|
|
||||||
#expect(json?["type"] as? String != nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Manages subscriptions correctly")
|
|
||||||
func subscriptionManagement() async throws {
|
|
||||||
// Arrange
|
|
||||||
let client = BufferWebSocketClient()
|
|
||||||
var subscriber1Count = 0
|
|
||||||
var subscriber2Count = 0
|
|
||||||
|
|
||||||
// Act
|
|
||||||
client.subscribe(id: "sub1") { _ in
|
|
||||||
subscriber1Count += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
client.subscribe(id: "sub2") { _ in
|
|
||||||
subscriber2Count += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate an event (would normally come through WebSocket)
|
|
||||||
// For testing purposes, we'd need to expose internal methods
|
|
||||||
|
|
||||||
// Remove one subscription
|
|
||||||
client.unsubscribe(id: "sub1")
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// After unsubscribing sub1, only sub2 should receive events
|
|
||||||
// This would be verified in a full integration test
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Handles reconnection with exponential backoff")
|
|
||||||
func reconnectionLogic() async throws {
|
|
||||||
// Arrange
|
|
||||||
let client = BufferWebSocketClient()
|
|
||||||
saveTestServerConfig()
|
saveTestServerConfig()
|
||||||
|
|
||||||
// Act
|
// Connect
|
||||||
client.connect()
|
client.connect()
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
// Simulate disconnection
|
let mockWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
// In a real test, we'd trigger this through the WebSocket mock
|
#expect(mockWebSocket?.isConnected == true)
|
||||||
|
|
||||||
// Assert
|
// Act - Simulate ping message
|
||||||
// Verify reconnection attempts happen with increasing delays
|
mockWebSocket?.simulateMessage(.string("{\"type\":\"ping\"}"))
|
||||||
// This would require exposing reconnection state or using time-based testing
|
|
||||||
|
// Wait for processing
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
||||||
|
|
||||||
|
// Assert - Check if pong was sent
|
||||||
|
let sentMessages = mockWebSocket?.sentJSONMessages() ?? []
|
||||||
|
#expect(sentMessages.contains { $0["type"] as? String == "pong" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Cleans up resources on disconnect")
|
@Test("Subscribes to sessions correctly")
|
||||||
func disconnectCleanup() async throws {
|
func sessionSubscription() async throws {
|
||||||
// Arrange
|
// Arrange
|
||||||
let client = BufferWebSocketClient()
|
let mockFactory = MockWebSocketFactory()
|
||||||
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
saveTestServerConfig()
|
saveTestServerConfig()
|
||||||
|
|
||||||
|
let sessionId = "test-session-456"
|
||||||
var eventReceived = false
|
var eventReceived = false
|
||||||
client.subscribe(id: "test") { _ in
|
|
||||||
|
// Act
|
||||||
|
client.subscribe(to: sessionId) { _ in
|
||||||
eventReceived = true
|
eventReceived = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.connect()
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
|
let mockWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
|
|
||||||
|
// Assert - Check if subscribe message was sent
|
||||||
|
let sentMessages = mockWebSocket?.sentJSONMessages() ?? []
|
||||||
|
#expect(sentMessages.contains { msg in
|
||||||
|
msg["type"] as? String == "subscribe" &&
|
||||||
|
msg["sessionId"] as? String == sessionId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Handles reconnection after disconnection")
|
||||||
|
func reconnection() async throws {
|
||||||
|
// Arrange
|
||||||
|
let mockFactory = MockWebSocketFactory()
|
||||||
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
|
saveTestServerConfig()
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
client.connect()
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
|
let firstWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
|
#expect(client.isConnected == true)
|
||||||
|
|
||||||
|
// Act - Simulate disconnection
|
||||||
|
firstWebSocket?.simulateDisconnection()
|
||||||
|
|
||||||
|
// Wait for reconnection attempt
|
||||||
|
try await Task.sleep(nanoseconds: 2_000_000_000) // 2s
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
#expect(mockFactory.createdWebSockets.count > 1)
|
||||||
|
let secondWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
|
#expect(secondWebSocket !== firstWebSocket)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Sends ping messages periodically")
|
||||||
|
func pingMessages() async throws {
|
||||||
|
// Arrange
|
||||||
|
let mockFactory = MockWebSocketFactory()
|
||||||
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
|
saveTestServerConfig()
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
client.connect()
|
client.connect()
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
|
let mockWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
|
let initialPingCount = mockWebSocket?.pingCount ?? 0
|
||||||
|
|
||||||
|
// Assert - Initial ping during connection
|
||||||
|
#expect(initialPingCount > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Unsubscribes from sessions correctly")
|
||||||
|
func sessionUnsubscription() async throws {
|
||||||
|
// Arrange
|
||||||
|
let mockFactory = MockWebSocketFactory()
|
||||||
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
|
saveTestServerConfig()
|
||||||
|
|
||||||
|
let sessionId = "test-session-789"
|
||||||
|
|
||||||
|
// Subscribe first
|
||||||
|
client.subscribe(to: sessionId) { _ in }
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
client.connect()
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
|
let mockWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
|
|
||||||
|
// Clear sent messages
|
||||||
|
mockWebSocket?.sentMessages.removeAll()
|
||||||
|
|
||||||
|
// Act - Unsubscribe
|
||||||
|
client.unsubscribe(from: sessionId)
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let sentMessages = mockWebSocket?.sentJSONMessages() ?? []
|
||||||
|
#expect(sentMessages.contains { msg in
|
||||||
|
msg["type"] as? String == "unsubscribe" &&
|
||||||
|
msg["sessionId"] as? String == sessionId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Cleans up on disconnect")
|
||||||
|
func cleanup() async throws {
|
||||||
|
// Arrange
|
||||||
|
let mockFactory = MockWebSocketFactory()
|
||||||
|
let client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
||||||
|
saveTestServerConfig()
|
||||||
|
|
||||||
|
// Subscribe to a session
|
||||||
|
client.subscribe(to: "test-session") { _ in }
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
client.connect()
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
|
|
||||||
|
let mockWebSocket = mockFactory.lastCreatedWebSocket
|
||||||
|
#expect(client.isConnected == true)
|
||||||
|
|
||||||
|
// Act
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
#expect(client.isConnected == false)
|
#expect(client.isConnected == false)
|
||||||
#expect(client.connectionError == nil)
|
#expect(mockWebSocket?.disconnectCalled == true)
|
||||||
|
#expect(mockWebSocket?.lastDisconnectCode == .goingAway)
|
||||||
// Verify subscriptions are maintained but not receiving events
|
|
||||||
// In a real test, we'd verify no events are delivered after disconnect
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Validates magic byte in binary messages")
|
|
||||||
func magicByteValidation() async throws {
|
|
||||||
// Arrange
|
|
||||||
var invalidData = Data([0xAB]) // Wrong magic byte
|
|
||||||
invalidData.append(contentsOf: [0, 0, 0, 0]) // Some dummy data
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
// In the real implementation, this should be rejected
|
|
||||||
#expect(invalidData.first != 0xBF)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Handles malformed JSON gracefully")
|
|
||||||
func malformedJSONHandling() async throws {
|
|
||||||
// Arrange
|
|
||||||
let malformedMessages = [
|
|
||||||
"not json",
|
|
||||||
"{invalid json}",
|
|
||||||
"""
|
|
||||||
{"type": }
|
|
||||||
""",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
for message in malformedMessages {
|
|
||||||
let data = message.data(using: .utf8) ?? Data()
|
|
||||||
let json = try? JSONSerialization.jsonObject(with: data)
|
|
||||||
#expect(json == nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Maintains connection with periodic pings")
|
// MARK: - Test Helpers
|
||||||
func pingMechanism() async throws {
|
|
||||||
// Arrange
|
|
||||||
let client = BufferWebSocketClient()
|
|
||||||
saveTestServerConfig()
|
|
||||||
|
|
||||||
// Act
|
|
||||||
client.connect()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// In a real test with mock WebSocket, we'd verify:
|
|
||||||
// 1. Ping messages are sent periodically
|
|
||||||
// 2. Connection stays alive with pings
|
|
||||||
// 3. Connection closes if pings fail
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
|
||||||
|
|
||||||
private func saveTestServerConfig() {
|
private func saveTestServerConfig() {
|
||||||
let config = TestFixtures.validServerConfig
|
let config = ServerConfig(
|
||||||
|
host: "localhost",
|
||||||
|
port: 8888,
|
||||||
|
useSSL: false,
|
||||||
|
username: nil,
|
||||||
|
password: nil
|
||||||
|
)
|
||||||
|
|
||||||
if let data = try? JSONEncoder().encode(config) {
|
if let data = try? JSONEncoder().encode(config) {
|
||||||
UserDefaults.standard.set(data, forKey: "savedServerConfig")
|
UserDefaults.standard.set(data, forKey: "savedServerConfig")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Integration Tests
|
|
||||||
|
|
||||||
@Suite("BufferWebSocketClient Integration Tests", .tags(.integration, .websocket))
|
|
||||||
@MainActor
|
|
||||||
struct BufferWebSocketClientIntegrationTests {
|
|
||||||
@Test("Full connection and message flow", .timeLimit(.seconds(5)))
|
|
||||||
func fullConnectionFlow() async throws {
|
|
||||||
// This test would require a mock WebSocket server
|
|
||||||
// or modifications to BufferWebSocketClient to accept mock dependencies
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
let client = BufferWebSocketClient()
|
|
||||||
let expectation = confirmation("Received buffer update")
|
|
||||||
|
|
||||||
client.subscribe(id: "integration-test") { event in
|
|
||||||
if case .bufferUpdate = event {
|
|
||||||
Task { await expectation.fulfill() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Act
|
|
||||||
// In a real integration test:
|
|
||||||
// 1. Start mock WebSocket server
|
|
||||||
// 2. Connect client
|
|
||||||
// 3. Send buffer update from server
|
|
||||||
// 4. Verify client receives and parses it correctly
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// await fulfillment(of: [expectation], timeout: .seconds(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue