mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-06-28 05:29:29 +00:00
332 lines
11 KiB
Swift
332 lines
11 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import VibeTunnel
|
|
|
|
@Suite("BufferWebSocketClient Tests", .tags(.critical, .websocket))
|
|
@MainActor
|
|
final class BufferWebSocketClientTests {
|
|
// Test dependencies
|
|
let mockFactory: MockWebSocketFactory
|
|
let client: BufferWebSocketClient
|
|
|
|
// Initialize test environment
|
|
init() {
|
|
mockFactory = MockWebSocketFactory()
|
|
client = BufferWebSocketClient(webSocketFactory: mockFactory)
|
|
|
|
// Setup test server configuration
|
|
TestFixtures.saveServerConfig(.init(
|
|
host: "localhost",
|
|
port: 8888,
|
|
name: nil,
|
|
password: nil
|
|
))
|
|
}
|
|
|
|
deinit {
|
|
// Cleanup
|
|
client.disconnect()
|
|
mockFactory.reset()
|
|
}
|
|
@Test("Connects successfully with valid configuration", .timeLimit(.seconds(5)))
|
|
func successfulConnection() async throws {
|
|
// Act
|
|
client.connect()
|
|
|
|
// Give it a moment to process
|
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
|
|
// Assert
|
|
#expect(mockFactory.createdWebSockets.count == 1)
|
|
|
|
let mockWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
#expect(mockWebSocket.isConnected)
|
|
#expect(mockWebSocket.lastConnectURL?.absoluteString.contains("/buffers") ?? false)
|
|
#expect(client.isConnected)
|
|
#expect(client.connectionError == nil)
|
|
}
|
|
|
|
@Test("Handles connection failure gracefully")
|
|
func connectionFailure() async throws {
|
|
// Act
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
|
|
|
let mockWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
mockWebSocket.simulateError(WebSocketError.connectionFailed)
|
|
|
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
|
|
|
// Assert
|
|
#expect(!client.isConnected)
|
|
#expect(client.connectionError != nil)
|
|
}
|
|
|
|
@Test("Parses binary buffer messages", arguments: [
|
|
(cols: 80, rows: 24),
|
|
(cols: 120, rows: 30),
|
|
(cols: 160, rows: 50)
|
|
])
|
|
func binaryMessageParsing(cols: Int, rows: Int) async throws {
|
|
// Arrange
|
|
var receivedEvent: TerminalWebSocketEvent?
|
|
let sessionId = "test-session-123"
|
|
|
|
// Subscribe to events
|
|
client.subscribe(to: sessionId) { event in
|
|
receivedEvent = event
|
|
}
|
|
|
|
// Connect
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
|
|
let mockWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
#expect(mockWebSocket.isConnected)
|
|
|
|
// Create test message
|
|
let bufferData = TestFixtures.bufferSnapshot(cols: cols, rows: rows)
|
|
let messageData = TestFixtures.wrappedBufferMessage(sessionId: sessionId, bufferData: bufferData)
|
|
|
|
// Act - Simulate receiving the message
|
|
mockWebSocket.simulateMessage(.data(messageData))
|
|
|
|
// Wait for processing
|
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
|
|
// Assert
|
|
let event = try #require(receivedEvent)
|
|
guard case .bufferUpdate(let snapshot) = event else {
|
|
Issue.record("Expected buffer update event, got \(event)")
|
|
return
|
|
}
|
|
|
|
#expect(snapshot.cols == cols)
|
|
#expect(snapshot.rows == rows)
|
|
}
|
|
|
|
@Test("Handles text messages", arguments: [
|
|
(type: "ping", expectedResponse: "pong"),
|
|
(type: "error", expectedResponse: nil)
|
|
])
|
|
func textMessageHandling(type: String, expectedResponse: String?) async throws {
|
|
// Connect
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
|
|
let mockWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
|
|
// Act - Simulate message
|
|
let message = TestFixtures.terminalEvent(type: type)
|
|
mockWebSocket.simulateMessage(.string(message))
|
|
|
|
// Wait for processing
|
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
|
|
|
// Assert
|
|
let sentMessages = mockWebSocket.sentJSONMessages()
|
|
|
|
if let expectedResponse = expectedResponse {
|
|
#expect(sentMessages.contains { $0["type"] as? String == expectedResponse })
|
|
} else {
|
|
// For error messages, we expect no response
|
|
#expect(!sentMessages.contains { $0["type"] as? String == type })
|
|
}
|
|
}
|
|
|
|
@Test("Subscribes to sessions correctly")
|
|
func sessionSubscription() async throws {
|
|
// Arrange
|
|
let sessionId = "test-session-456"
|
|
var eventReceived = false
|
|
|
|
// Act
|
|
client.subscribe(to: sessionId) { _ in
|
|
eventReceived = true
|
|
}
|
|
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
|
|
let mockWebSocket = try #require(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", .timeLimit(.seconds(3)))
|
|
func reconnection() async throws {
|
|
// Connect
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
|
|
let firstWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
#expect(client.isConnected)
|
|
|
|
// Act - Simulate disconnection
|
|
firstWebSocket.simulateDisconnection()
|
|
|
|
// Wait for reconnection attempt
|
|
try await waitFor { [weak self] in
|
|
(self?.mockFactory.createdWebSockets.count ?? 0) > 1
|
|
}
|
|
|
|
// Assert
|
|
let secondWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
#expect(secondWebSocket !== firstWebSocket)
|
|
}
|
|
|
|
@Test("Sends ping messages periodically", .disabled("Ping timing is unpredictable in tests"))
|
|
func pingMessages() async throws {
|
|
// Act
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
|
|
let mockWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
let initialPingCount = mockWebSocket.pingCount
|
|
|
|
// Wait longer to see if pings are sent
|
|
try await Task.sleep(for: .seconds(1))
|
|
|
|
// Assert - Should have sent at least one ping
|
|
#expect(mockWebSocket.pingCount > initialPingCount)
|
|
}
|
|
|
|
@Test("Unsubscribes from sessions correctly")
|
|
func sessionUnsubscription() async throws {
|
|
// Arrange
|
|
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 = try #require(mockFactory.lastCreatedWebSocket)
|
|
|
|
// Clear sent messages to isolate unsubscribe message
|
|
mockWebSocket.reset()
|
|
mockWebSocket.isConnected = true // Keep connected state
|
|
|
|
// 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 {
|
|
// Subscribe to a session
|
|
client.subscribe(to: "test-session") { _ in }
|
|
|
|
// Connect
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
|
|
let mockWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
#expect(client.isConnected)
|
|
|
|
// Act
|
|
client.disconnect()
|
|
|
|
// Assert
|
|
#expect(!client.isConnected)
|
|
#expect(mockWebSocket.disconnectCalled)
|
|
#expect(mockWebSocket.lastDisconnectCode == .goingAway)
|
|
}
|
|
|
|
// MARK: - Error Handling Tests
|
|
|
|
@Test("Handles invalid magic byte in binary messages")
|
|
func invalidMagicByte() async throws {
|
|
// Arrange
|
|
var receivedEvent: TerminalWebSocketEvent?
|
|
let sessionId = "test-session"
|
|
|
|
client.subscribe(to: sessionId) { event in
|
|
receivedEvent = event
|
|
}
|
|
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
let mockWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
|
|
// Create message with wrong magic byte
|
|
var messageData = Data()
|
|
messageData.append(0xFF) // Wrong magic byte
|
|
messageData.append(contentsOf: [0, 0, 0, 4]) // Session ID length
|
|
messageData.append("test".data(using: .utf8)!)
|
|
|
|
// Act
|
|
mockWebSocket.simulateMessage(.data(messageData))
|
|
try await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
// Assert - Should not receive any event
|
|
#expect(receivedEvent == nil)
|
|
}
|
|
|
|
@Test("Handles malformed buffer data gracefully")
|
|
func malformedBufferData() async throws {
|
|
// Arrange
|
|
var receivedEvent: TerminalWebSocketEvent?
|
|
let sessionId = "test-session"
|
|
|
|
client.subscribe(to: sessionId) { event in
|
|
receivedEvent = event
|
|
}
|
|
|
|
client.connect()
|
|
try await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
let mockWebSocket = try #require(mockFactory.lastCreatedWebSocket)
|
|
|
|
// Create message with valid wrapper but invalid buffer data
|
|
var bufferData = Data()
|
|
bufferData.append(contentsOf: [0xFF, 0xFF]) // Invalid magic for buffer
|
|
bufferData.append(contentsOf: [1, 2, 3, 4]) // Random data
|
|
|
|
let messageData = TestFixtures.wrappedBufferMessage(sessionId: sessionId, bufferData: bufferData)
|
|
|
|
// Act
|
|
mockWebSocket.simulateMessage(.data(messageData))
|
|
try await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
// Assert - Should not crash and not receive event
|
|
#expect(receivedEvent == nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Test Extensions
|
|
|
|
extension BufferWebSocketClientTests {
|
|
/// Wait for condition with timeout
|
|
func waitFor(
|
|
_ condition: @escaping () async -> Bool,
|
|
timeout: Duration = .seconds(5),
|
|
pollingInterval: Duration = .milliseconds(100)
|
|
) async throws {
|
|
let deadline = ContinuousClock.now.advanced(by: timeout)
|
|
|
|
while ContinuousClock.now < deadline {
|
|
if await condition() {
|
|
return
|
|
}
|
|
try await Task.sleep(for: pollingInterval)
|
|
}
|
|
|
|
Issue.record("Timeout waiting for condition")
|
|
}
|
|
}
|