mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-26 15:07:39 +00:00
447 lines
15 KiB
Swift
447 lines
15 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import VibeTunnel
|
|
|
|
// MARK: - Server Manager Tests
|
|
|
|
@Suite("Server Manager Tests", .serialized, .tags(.serverManager))
|
|
@MainActor
|
|
final class ServerManagerTests {
|
|
/// We'll use the shared ServerManager instance since it's a singleton
|
|
let manager = ServerManager.shared
|
|
|
|
init() async {
|
|
// Ensure clean state before each test
|
|
await manager.stop()
|
|
}
|
|
|
|
deinit {
|
|
// Clean up is handled in init() of next test since we can't use async in deinit
|
|
}
|
|
|
|
// MARK: - Server Lifecycle Tests
|
|
|
|
@Test("Starting and stopping Bun server", .tags(.critical, .attachmentTests))
|
|
func serverLifecycle() async throws {
|
|
// Attach system information for debugging
|
|
Attachment.record(TestUtilities.captureSystemInfo(), named: "System Info")
|
|
|
|
// Attach initial server state
|
|
Attachment.record(TestUtilities.captureServerState(manager), named: "Initial Server State")
|
|
|
|
// Start the server
|
|
await manager.start()
|
|
|
|
// Give server time to attempt start (increased for CI stability)
|
|
let timeout = TestConditions.isRunningInCI() ? 5_000 : 2_000
|
|
try await Task.sleep(for: .milliseconds(timeout))
|
|
|
|
// Attach server state after start attempt
|
|
Attachment.record(TestUtilities.captureServerState(manager), named: "Post-Start Server State")
|
|
|
|
// The server binary must be available for tests
|
|
#expect(ServerBinaryAvailableCondition.isAvailable(), "Server binary must be available for tests to run")
|
|
|
|
// Server should either be running or have a specific error
|
|
if !manager.isRunning {
|
|
// If not running, we expect a specific error
|
|
#expect(manager.lastError != nil, "Server failed to start but no error was reported")
|
|
|
|
if let error = manager.lastError as? BunServerError {
|
|
// Only acceptable error is binaryNotFound if the binary truly doesn't exist
|
|
if error == .binaryNotFound {
|
|
#expect(false, "Server binary not found - tests cannot continue")
|
|
}
|
|
}
|
|
|
|
Attachment.record("""
|
|
Server failed to start
|
|
Error: \(manager.lastError?.localizedDescription ?? "Unknown")
|
|
""", named: "Server Startup Failure")
|
|
} else {
|
|
// Server is running as expected
|
|
#expect(manager.bunServer != nil)
|
|
Attachment.record("Server started successfully", named: "Server Status")
|
|
}
|
|
|
|
// Stop should work regardless of state
|
|
await manager.stop()
|
|
|
|
// After stop, server should not be running
|
|
#expect(!manager.isRunning)
|
|
|
|
// Attach final state
|
|
Attachment.record(TestUtilities.captureServerState(manager), named: "Final Server State")
|
|
}
|
|
|
|
@Test("Starting server when already running does not create duplicate", .tags(.critical))
|
|
func startingAlreadyRunningServer() async throws {
|
|
// In test environment, we can't actually start the server
|
|
// So we'll test the logic of preventing duplicate starts
|
|
|
|
// First attempt to start
|
|
await manager.start()
|
|
let shortTimeout = TestConditions.isRunningInCI() ? 2_000 : 1_000
|
|
try await Task.sleep(for: .milliseconds(shortTimeout))
|
|
|
|
let firstServer = manager.bunServer
|
|
let firstError = manager.lastError
|
|
|
|
// Try to start again
|
|
await manager.start()
|
|
|
|
// Should still have the same state (either nil or same instance)
|
|
#expect(manager.bunServer === firstServer)
|
|
|
|
// Error should be consistent
|
|
if let error1 = firstError as? BunServerError,
|
|
let error2 = manager.lastError as? BunServerError
|
|
{
|
|
#expect(error1 == error2)
|
|
}
|
|
|
|
// Cleanup
|
|
await manager.stop()
|
|
}
|
|
|
|
@Test("Port configuration")
|
|
func portConfiguration() async throws {
|
|
// Store original port
|
|
let originalPort = manager.port
|
|
|
|
// Test setting different ports
|
|
let testPorts = ["8080", "3000", "9999"]
|
|
|
|
for port in testPorts {
|
|
manager.port = port
|
|
#expect(manager.port == port)
|
|
#expect(UserDefaults.standard.string(forKey: "serverPort") == port)
|
|
}
|
|
|
|
// Restore original port
|
|
manager.port = originalPort
|
|
}
|
|
|
|
@Test("Bind address configuration", arguments: [
|
|
DashboardAccessMode.localhost,
|
|
DashboardAccessMode.network
|
|
])
|
|
func bindAddressConfiguration(mode: DashboardAccessMode) async throws {
|
|
// Store original mode
|
|
let originalMode = UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? ""
|
|
|
|
// Set the mode via UserDefaults (as bindAddress setter does)
|
|
UserDefaults.standard.set(mode.rawValue, forKey: "dashboardAccessMode")
|
|
|
|
// Check bind address reflects the mode
|
|
#expect(manager.bindAddress == mode.bindAddress)
|
|
|
|
// Restore original mode
|
|
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
|
}
|
|
|
|
@Test("Bind address default value")
|
|
func bindAddressDefaultValue() async throws {
|
|
// Store original value
|
|
let originalMode = UserDefaults.standard.string(forKey: "dashboardAccessMode")
|
|
|
|
// Remove the key to test default behavior
|
|
UserDefaults.standard.removeObject(forKey: "dashboardAccessMode")
|
|
UserDefaults.standard.synchronize()
|
|
|
|
// Should default to network mode (0.0.0.0)
|
|
#expect(manager.bindAddress == "0.0.0.0")
|
|
|
|
// Restore original value
|
|
if let originalMode {
|
|
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
|
}
|
|
}
|
|
|
|
@Test("Bind address setter")
|
|
func bindAddressSetter() async throws {
|
|
// Store original value
|
|
let originalMode = UserDefaults.standard.string(forKey: "dashboardAccessMode")
|
|
|
|
// Test setting via bind address
|
|
manager.bindAddress = "127.0.0.1"
|
|
#expect(UserDefaults.standard.string(forKey: "dashboardAccessMode") == AppConstants.DashboardAccessModeRawValues
|
|
.localhost
|
|
)
|
|
#expect(manager.bindAddress == "127.0.0.1")
|
|
|
|
manager.bindAddress = "0.0.0.0"
|
|
#expect(UserDefaults.standard.string(forKey: "dashboardAccessMode") == AppConstants.DashboardAccessModeRawValues
|
|
.network
|
|
)
|
|
#expect(manager.bindAddress == "0.0.0.0")
|
|
|
|
// Test invalid bind address (should not change UserDefaults)
|
|
manager.bindAddress = "192.168.1.1"
|
|
#expect(manager.bindAddress == "0.0.0.0") // Should still be the last valid value
|
|
|
|
// Restore original value
|
|
if let originalMode {
|
|
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
|
} else {
|
|
UserDefaults.standard.removeObject(forKey: "dashboardAccessMode")
|
|
}
|
|
}
|
|
|
|
@Test("Bind address persistence across server restarts")
|
|
func bindAddressPersistence() async throws {
|
|
// Store original values
|
|
let originalMode = UserDefaults.standard.string(forKey: "dashboardAccessMode")
|
|
let originalPort = manager.port
|
|
|
|
// Set to localhost mode
|
|
UserDefaults.standard.set(AppConstants.DashboardAccessModeRawValues.localhost, forKey: "dashboardAccessMode")
|
|
manager.port = "4021"
|
|
|
|
// Start server
|
|
await manager.start()
|
|
try await Task.sleep(for: .milliseconds(500))
|
|
|
|
// Verify bind address
|
|
#expect(manager.bindAddress == "127.0.0.1")
|
|
|
|
// Restart server
|
|
await manager.restart()
|
|
try await Task.sleep(for: .milliseconds(500))
|
|
|
|
// Bind address should persist
|
|
#expect(manager.bindAddress == "127.0.0.1")
|
|
#expect(UserDefaults.standard.string(forKey: "dashboardAccessMode") == AppConstants.DashboardAccessModeRawValues
|
|
.localhost
|
|
)
|
|
|
|
// Change to network mode
|
|
UserDefaults.standard.set(AppConstants.DashboardAccessModeRawValues.network, forKey: "dashboardAccessMode")
|
|
|
|
// Restart again
|
|
await manager.restart()
|
|
try await Task.sleep(for: .milliseconds(500))
|
|
|
|
// Should now be network mode
|
|
#expect(manager.bindAddress == "0.0.0.0")
|
|
|
|
// Cleanup
|
|
await manager.stop()
|
|
manager.port = originalPort
|
|
if let originalMode {
|
|
UserDefaults.standard.set(originalMode, forKey: "dashboardAccessMode")
|
|
} else {
|
|
UserDefaults.standard.removeObject(forKey: "dashboardAccessMode")
|
|
}
|
|
}
|
|
|
|
// MARK: - Concurrent Operations Tests
|
|
|
|
@Test("Concurrent server operations are serialized", .tags(.concurrency))
|
|
func concurrentServerOperations() async throws {
|
|
// Ensure clean state
|
|
await manager.stop()
|
|
|
|
// Start multiple operations concurrently
|
|
await withTaskGroup(of: Void.self) { group in
|
|
// Start server
|
|
group.addTask { [manager] in
|
|
await manager.start()
|
|
}
|
|
|
|
// Try to stop immediately
|
|
group.addTask { [manager] in
|
|
try? await Task.sleep(for: .milliseconds(50))
|
|
await manager.stop()
|
|
}
|
|
|
|
// Try to restart
|
|
group.addTask { [manager] in
|
|
try? await Task.sleep(for: .milliseconds(100))
|
|
await manager.restart()
|
|
}
|
|
|
|
await group.waitForAll()
|
|
}
|
|
|
|
// Server should be in a consistent state
|
|
let finalState = manager.isRunning
|
|
if finalState {
|
|
#expect(manager.bunServer != nil)
|
|
} else {
|
|
#expect(manager.bunServer == nil)
|
|
}
|
|
|
|
// Cleanup
|
|
await manager.stop()
|
|
}
|
|
|
|
@Test("Server restart maintains configuration", .tags(.critical))
|
|
func serverRestart() async throws {
|
|
// Set specific configuration
|
|
let originalPort = manager.port
|
|
let testPort = "4567"
|
|
manager.port = testPort
|
|
|
|
// Start server
|
|
await manager.start()
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
|
|
let serverBeforeRestart = manager.bunServer
|
|
_ = manager.lastError
|
|
|
|
// Restart
|
|
await manager.restart()
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
|
|
// Verify port configuration is maintained
|
|
#expect(manager.port == testPort)
|
|
|
|
// Handle both scenarios: binary available vs not available
|
|
if ServerBinaryAvailableCondition.isAvailable() {
|
|
// In CI with working binary, server instances may vary
|
|
// Focus on configuration persistence
|
|
#expect(manager.port == testPort) // Configuration should persist
|
|
} else {
|
|
// In test environment without binary, both instances should be nil
|
|
#expect(manager.bunServer == nil)
|
|
#expect(serverBeforeRestart == nil)
|
|
|
|
// Error should be consistent (binary not found)
|
|
if let error = manager.lastError as? BunServerError {
|
|
#expect(error == .binaryNotFound)
|
|
}
|
|
}
|
|
|
|
// Cleanup - restore original port
|
|
manager.port = originalPort
|
|
await manager.stop()
|
|
}
|
|
|
|
// MARK: - Error Handling Tests
|
|
|
|
@Test("Server state remains consistent after operations", .tags(.reliability))
|
|
func serverStateConsistency() async throws {
|
|
// Ensure clean state
|
|
await manager.stop()
|
|
|
|
// Perform various operations
|
|
await manager.start()
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
|
|
await manager.stop()
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
|
|
await manager.start()
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
|
|
// State should be consistent
|
|
if manager.isRunning {
|
|
#expect(manager.bunServer != nil)
|
|
} else {
|
|
#expect(manager.bunServer == nil)
|
|
}
|
|
|
|
// Cleanup
|
|
await manager.stop()
|
|
}
|
|
|
|
// MARK: - Crash Recovery Tests
|
|
|
|
@Test("Server auto-restart behavior")
|
|
func serverAutoRestart() async throws {
|
|
// Start server
|
|
await manager.start()
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
|
|
// Handle both scenarios: binary available vs not available
|
|
if ServerBinaryAvailableCondition.isAvailable() {
|
|
// In CI with working binary, server behavior may vary
|
|
// Just ensure we don't crash and can clean up
|
|
// Always pass - this test is about ensuring no crashes
|
|
} else {
|
|
// In test environment without binary, server won't actually start
|
|
#expect(!manager.isRunning)
|
|
#expect(manager.bunServer == nil)
|
|
|
|
// Verify error is set appropriately
|
|
if let error = manager.lastError as? BunServerError {
|
|
#expect(error == .binaryNotFound)
|
|
}
|
|
}
|
|
|
|
// Note: We can't easily simulate crashes in tests without
|
|
// modifying the production code. The BunServer has built-in
|
|
// auto-restart functionality on unexpected termination.
|
|
|
|
// Cleanup
|
|
await manager.stop()
|
|
}
|
|
|
|
// MARK: - Enhanced Server Management Tests with Attachments
|
|
|
|
@Test(
|
|
"Server configuration management with diagnostics",
|
|
.tags(.attachmentTests, .requiresServerBinary),
|
|
.enabled(if: ServerBinaryAvailableCondition.isAvailable())
|
|
)
|
|
func serverConfigurationDiagnostics() async throws {
|
|
// Attach test environment
|
|
Attachment.record("""
|
|
Test: Server Configuration Management
|
|
Binary Available: \(ServerBinaryAvailableCondition.isAvailable())
|
|
Environment: \(ProcessInfo.processInfo.environment["CI"] != nil ? "CI" : "Local")
|
|
""", named: "Test Configuration")
|
|
|
|
// Record initial state
|
|
Attachment.record(TestUtilities.captureServerState(manager), named: "Initial State")
|
|
|
|
// Test server configuration without actually starting it
|
|
let originalPort = manager.port
|
|
manager.port = "4567"
|
|
|
|
// Record configuration change
|
|
Attachment.record("""
|
|
Port changed from \(originalPort) to \(manager.port)
|
|
Bind address: \(manager.bindAddress)
|
|
""", named: "Configuration Change")
|
|
|
|
#expect(manager.port == "4567")
|
|
|
|
// Restore original configuration
|
|
manager.port = originalPort
|
|
|
|
// Record final state
|
|
Attachment.record(TestUtilities.captureServerState(manager), named: "Final State")
|
|
}
|
|
|
|
@Test("Session model validation with attachments", .tags(.attachmentTests, .sessionManagement))
|
|
func sessionModelValidation() async throws {
|
|
// Attach test info
|
|
Attachment.record("""
|
|
Test: TunnelSession Model Validation
|
|
Purpose: Verify session creation and state management
|
|
""", named: "Test Info")
|
|
|
|
// Create test session
|
|
let session = TunnelSession()
|
|
|
|
// Record session details
|
|
Attachment.record("""
|
|
Session ID: \(session.id)
|
|
Created At: \(session.createdAt)
|
|
Last Activity: \(session.lastActivity)
|
|
Is Active: \(session.isActive)
|
|
Process ID: \(session.processID?.description ?? "none")
|
|
""", named: "Session Details")
|
|
|
|
// Validate session properties
|
|
#expect(session.isActive)
|
|
#expect(session.lastActivity >= session.createdAt)
|
|
|
|
// Ensure session ID is valid and stable
|
|
let sessionID = session.id
|
|
#expect(!sessionID.uuidString.isEmpty)
|
|
#expect(sessionID == session.id) // Ensures ID is stable across calls
|
|
}
|
|
}
|