vibetunnel/ios/VibeTunnelTests/WebSocketReconnectionTests.swift
2025-06-21 14:39:44 +02:00

332 lines
9.7 KiB
Swift

import Foundation
import Testing
@Suite("WebSocket Reconnection Tests", .tags(.critical, .websocket))
struct WebSocketReconnectionTests {
// MARK: - Reconnection Strategy Tests
@Test("Exponential backoff calculation")
func exponentialBackoff() {
// Test exponential backoff with jitter
let baseDelay = 1.0
let maxDelay = 60.0
// Calculate delays for multiple attempts
var delays: [Double] = []
for attempt in 0..<10 {
let delay = min(baseDelay * pow(2.0, Double(attempt)), maxDelay)
let jitteredDelay = delay * (0.5 + Double.random(in: 0...0.5))
delays.append(jitteredDelay)
// Verify bounds
#expect(jitteredDelay >= baseDelay * 0.5)
#expect(jitteredDelay <= maxDelay)
}
// Verify progression (later delays should generally be larger)
for i in 1..<5 {
#expect(delays[i] >= delays[0])
}
}
@Test("Maximum retry attempts")
func maxRetryAttempts() {
let maxAttempts = 5
var attempts = 0
var shouldRetry = true
while shouldRetry && attempts < maxAttempts {
attempts += 1
shouldRetry = attempts < maxAttempts
}
#expect(attempts == maxAttempts)
#expect(!shouldRetry)
}
// MARK: - Connection State Management
@Test("Connection state transitions")
func connectionStateTransitions() {
enum ConnectionState {
case disconnected
case connecting
case connected
case reconnecting
case failed
}
// Test valid transitions
var state = ConnectionState.disconnected
// Disconnected -> Connecting
state = .connecting
#expect(state == .connecting)
// Connecting -> Connected
state = .connected
#expect(state == .connected)
// Connected -> Reconnecting (on disconnect)
state = .reconnecting
#expect(state == .reconnecting)
// Reconnecting -> Connected
state = .connected
#expect(state == .connected)
// Any state -> Failed (on max retries)
state = .failed
#expect(state == .failed)
}
@Test("Connection lifecycle events")
func connectionLifecycle() {
var events: [String] = []
// Simulate connection lifecycle
events.append("will_connect")
events.append("did_connect")
events.append("did_disconnect")
events.append("will_reconnect")
events.append("did_reconnect")
#expect(events.count == 5)
#expect(events[0] == "will_connect")
#expect(events[1] == "did_connect")
#expect(events[2] == "did_disconnect")
#expect(events[3] == "will_reconnect")
#expect(events[4] == "did_reconnect")
}
// MARK: - Message Queue Management
@Test("Message queuing during disconnection")
func messageQueueing() {
var messageQueue: [String] = []
var isConnected = false
func sendMessage(_ message: String) {
if isConnected {
// Send immediately
#expect(messageQueue.isEmpty)
} else {
// Queue for later
messageQueue.append(message)
}
}
// Queue messages while disconnected
sendMessage("message1")
sendMessage("message2")
sendMessage("message3")
#expect(messageQueue.count == 3)
#expect(messageQueue[0] == "message1")
// Connect and flush queue
isConnected = true
let flushedMessages = messageQueue
messageQueue.removeAll()
#expect(flushedMessages.count == 3)
#expect(messageQueue.isEmpty)
}
@Test("Message queue size limits")
func messageQueueLimits() {
let maxQueueSize = 100
var messageQueue: [String] = []
// Fill queue beyond limit
for i in 0..<150 {
if messageQueue.count < maxQueueSize {
messageQueue.append("message\(i)")
}
}
#expect(messageQueue.count == maxQueueSize)
#expect(messageQueue.first == "message0")
#expect(messageQueue.last == "message99")
}
// MARK: - Reconnection Scenarios
@Test("Immediate reconnection on clean disconnect")
func cleanDisconnectReconnection() {
var reconnectDelay: TimeInterval = 0
let wasCleanDisconnect = true
if wasCleanDisconnect {
reconnectDelay = 0.1 // Minimal delay for clean disconnects
} else {
reconnectDelay = 1.0 // Standard delay for unexpected disconnects
}
#expect(reconnectDelay == 0.1)
}
@Test("Reconnection with authentication")
func reconnectionWithAuth() {
struct ConnectionConfig {
let url: String
let authToken: String?
let sessionId: String?
}
let config = ConnectionConfig(
url: "wss://localhost:8888/buffers",
authToken: "test-token",
sessionId: "session-123"
)
// Verify auth info is preserved for reconnection
#expect(config.authToken != nil)
#expect(config.sessionId != nil)
// Simulate reconnection with same config
let reconnectConfig = config
#expect(reconnectConfig.authToken == config.authToken)
#expect(reconnectConfig.sessionId == config.sessionId)
}
// MARK: - Error Recovery
@Test("Connection error categorization")
func errorCategorization() {
enum ConnectionError {
case network(String)
case authentication(String)
case server(Int)
case client(String)
}
func shouldRetry(error: ConnectionError) -> Bool {
switch error {
case .network:
true // Always retry network errors
case .authentication:
false // Don't retry auth errors
case .server(let code):
code >= 500 // Retry server errors
case .client:
false // Don't retry client errors
}
}
#expect(shouldRetry(error: .network("timeout")) == true)
#expect(shouldRetry(error: .authentication("invalid token")) == false)
#expect(shouldRetry(error: .server(500)) == true)
#expect(shouldRetry(error: .server(503)) == true)
#expect(shouldRetry(error: .server(400)) == false)
#expect(shouldRetry(error: .client("bad request")) == false)
}
@Test("Connection health monitoring")
func healthMonitoring() {
var lastPingTime = Date()
let pingInterval: TimeInterval = 30
let pingTimeout: TimeInterval = 10
// Simulate successful ping
lastPingTime = Date()
let timeSinceLastPing = Date().timeIntervalSince(lastPingTime)
#expect(timeSinceLastPing < pingTimeout)
// Simulate missed ping
lastPingTime = Date().addingTimeInterval(-40)
let missedPingTime = Date().timeIntervalSince(lastPingTime)
#expect(missedPingTime > pingInterval)
#expect(missedPingTime > pingTimeout)
}
// MARK: - State Persistence
@Test("Connection state persistence")
func statePersistence() {
struct ConnectionState: Codable {
let url: String
let sessionId: String?
let lastConnected: Date
let reconnectCount: Int
}
let state = ConnectionState(
url: "wss://localhost:8888",
sessionId: "abc123",
lastConnected: Date(),
reconnectCount: 3
)
// Encode
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try? encoder.encode(state)
#expect(data != nil)
// Decode
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try? decoder.decode(ConnectionState.self, from: data!)
#expect(decoded?.url == state.url)
#expect(decoded?.sessionId == state.sessionId)
#expect(decoded?.reconnectCount == state.reconnectCount)
}
// MARK: - Circuit Breaker Pattern
@Test("Circuit breaker for repeated failures")
func circuitBreaker() {
class CircuitBreaker {
private var failureCount = 0
private let failureThreshold = 5
private let resetTimeout: TimeInterval = 60
private var lastFailureTime: Date?
enum State {
case closed // Normal operation
case open // Failing, reject requests
case halfOpen // Testing if service recovered
}
var state: State {
if let lastFailure = lastFailureTime {
let timeSinceFailure = Date().timeIntervalSince(lastFailure)
if timeSinceFailure > resetTimeout {
return .halfOpen
}
}
return failureCount >= failureThreshold ? .open : .closed
}
func recordSuccess() {
failureCount = 0
lastFailureTime = nil
}
func recordFailure() {
failureCount += 1
lastFailureTime = Date()
}
}
let breaker = CircuitBreaker()
// Test normal state
#expect(breaker.state == .closed)
// Record failures
for _ in 0..<5 {
breaker.recordFailure()
}
// Circuit should be open
#expect(breaker.state == .open)
// Record success resets the breaker
breaker.recordSuccess()
#expect(breaker.state == .closed)
}
}