mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
330 lines
11 KiB
Swift
330 lines
11 KiB
Swift
import Foundation
|
|
import Network
|
|
import Testing
|
|
@testable import VibeTunnel
|
|
|
|
// MARK: - Mock Network Utility for Testing
|
|
|
|
@MainActor
|
|
enum MockNetworkUtility {
|
|
static var mockLocalIP: String?
|
|
static var mockAllIPs: [String] = []
|
|
static var shouldFailGetAddresses = false
|
|
|
|
static func reset() {
|
|
mockLocalIP = nil
|
|
mockAllIPs = []
|
|
shouldFailGetAddresses = false
|
|
}
|
|
|
|
static func getLocalIPAddress() -> String? {
|
|
if shouldFailGetAddresses { return nil }
|
|
return mockLocalIP
|
|
}
|
|
|
|
static func getAllIPAddresses() -> [String] {
|
|
if shouldFailGetAddresses { return [] }
|
|
return mockAllIPs
|
|
}
|
|
}
|
|
|
|
// MARK: - Network Utility Tests
|
|
|
|
@Suite("Network Utility Tests", .tags(.networking))
|
|
struct NetworkUtilityTests {
|
|
// MARK: - Local IP Address Tests
|
|
|
|
@Test("Get local IP address")
|
|
func testGetLocalIPAddress() throws {
|
|
// Test real implementation
|
|
let localIP = NetworkUtility.getLocalIPAddress()
|
|
|
|
// On a real system, we should get some IP address
|
|
// It might be nil in some test environments
|
|
if let ip = localIP {
|
|
#expect(!ip.isEmpty)
|
|
|
|
// Should be a valid IPv4 address format
|
|
let components = ip.split(separator: ".")
|
|
#expect(components.count == 4)
|
|
|
|
// Each component should be a valid number 0-255
|
|
for component in components {
|
|
if let num = Int(component) {
|
|
#expect(num >= 0 && num <= 255)
|
|
} else {
|
|
Issue.record("Invalid IP component: \(component)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("Local IP address preferences")
|
|
func localIPPreferences() throws {
|
|
// Test that we prefer local network addresses
|
|
let mockIPs = [
|
|
"192.168.1.100", // Preferred - local network
|
|
"10.0.0.50", // Preferred - local network
|
|
"172.16.0.10", // Preferred - local network
|
|
"8.8.8.8", // Not preferred - public IP
|
|
"127.0.0.1" // Should be filtered out - loopback
|
|
]
|
|
|
|
// Verify our preference logic
|
|
for ip in mockIPs {
|
|
if ip.hasPrefix("192.168.") || ip.hasPrefix("10.") || ip.hasPrefix("172.") {
|
|
#expect(Bool(true), "IP \(ip) should be preferred")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("Get all IP addresses")
|
|
func testGetAllIPAddresses() throws {
|
|
let allIPs = NetworkUtility.getAllIPAddresses()
|
|
|
|
// Should return array (might be empty in test environment)
|
|
#expect(allIPs.count >= 0)
|
|
|
|
// If we have IPs, verify they're valid
|
|
for ip in allIPs {
|
|
#expect(!ip.isEmpty)
|
|
|
|
// Should not contain loopback
|
|
#expect(!ip.hasPrefix("127."))
|
|
|
|
// Should be valid IPv4 format
|
|
let components = ip.split(separator: ".")
|
|
#expect(components.count == 4)
|
|
}
|
|
}
|
|
|
|
// MARK: - Network Interface Tests
|
|
|
|
@Test("Network interface filtering")
|
|
func interfaceFiltering() throws {
|
|
// Test that we filter interfaces correctly
|
|
let allIPs = NetworkUtility.getAllIPAddresses()
|
|
|
|
// Should not contain any loopback addresses
|
|
for ip in allIPs {
|
|
#expect(!ip.hasPrefix("127.0.0"))
|
|
#expect(ip != "::1") // IPv6 loopback
|
|
}
|
|
}
|
|
|
|
@Test("IPv4 address validation")
|
|
func iPv4Validation() throws {
|
|
let testIPs = [
|
|
("192.168.1.1", true),
|
|
("10.0.0.1", true),
|
|
("172.16.0.1", true),
|
|
("256.1.1.1", false), // Invalid - component > 255
|
|
("1.1.1", false), // Invalid - only 3 components
|
|
("1.1.1.1.1", false), // Invalid - too many components
|
|
("a.b.c.d", false), // Invalid - non-numeric
|
|
("", false) // Invalid - empty
|
|
]
|
|
|
|
for (ip, shouldBeValid) in testIPs {
|
|
let components = ip.split(separator: ".")
|
|
let isValid = components.count == 4 && components.allSatisfy { component in
|
|
if let num = Int(component) {
|
|
return num >= 0 && num <= 255
|
|
}
|
|
return false
|
|
}
|
|
|
|
#expect(isValid == shouldBeValid, "IP \(ip) validation failed")
|
|
}
|
|
}
|
|
|
|
// MARK: - Edge Cases Tests
|
|
|
|
@Test("Handle no network interfaces")
|
|
@MainActor
|
|
func noNetworkInterfaces() throws {
|
|
// In a real scenario where no interfaces are available
|
|
// the functions should return nil/empty array gracefully
|
|
|
|
MockNetworkUtility.shouldFailGetAddresses = true
|
|
|
|
#expect(MockNetworkUtility.getLocalIPAddress() == nil)
|
|
#expect(MockNetworkUtility.getAllIPAddresses().isEmpty)
|
|
|
|
MockNetworkUtility.reset()
|
|
}
|
|
|
|
@Test("Multiple network interfaces")
|
|
@MainActor
|
|
func multipleInterfaces() throws {
|
|
// When multiple interfaces exist, we should get all of them
|
|
MockNetworkUtility.mockAllIPs = [
|
|
"192.168.1.100", // Wi-Fi
|
|
"192.168.2.50", // Ethernet
|
|
"10.0.0.100" // VPN
|
|
]
|
|
|
|
let allIPs = MockNetworkUtility.getAllIPAddresses()
|
|
#expect(allIPs.count == 3)
|
|
#expect(Set(allIPs).count == 3) // All unique
|
|
|
|
MockNetworkUtility.reset()
|
|
}
|
|
|
|
// MARK: - Platform-Specific Tests
|
|
|
|
@Test("macOS network interface names")
|
|
func macOSInterfaceNames() throws {
|
|
// On macOS, typical interface names are:
|
|
// en0 - Primary network interface (often Wi-Fi)
|
|
// en1 - Secondary network interface (often Ethernet)
|
|
// en2, en3, etc. - Additional interfaces
|
|
|
|
// This test documents expected behavior
|
|
let expectedPrefixes = ["en"]
|
|
|
|
for prefix in expectedPrefixes {
|
|
#expect(prefix.hasPrefix("en"), "Network interfaces should start with 'en' on macOS")
|
|
}
|
|
}
|
|
|
|
// MARK: - Performance Tests
|
|
|
|
@Test("Performance of IP address retrieval", .tags(.performance, .attachmentTests))
|
|
func iPRetrievalPerformance() async throws {
|
|
// Enhanced performance testing with detailed metrics
|
|
var timings: [TimeInterval] = []
|
|
let iterations = 50
|
|
|
|
// Attach system configuration
|
|
Attachment.record("""
|
|
Test: IP Address Retrieval Performance
|
|
Iterations: \(iterations)
|
|
Test Environment: \(ProcessInfo.processInfo.environment["CI"] != nil ? "CI" : "Local")
|
|
System: \(TestUtilities.captureSystemInfo())
|
|
Network: \(TestUtilities.captureNetworkConfig())
|
|
""", named: "Performance Test Configuration")
|
|
|
|
// Measure individual timings
|
|
for _ in 0..<iterations {
|
|
let start = CFAbsoluteTimeGetCurrent()
|
|
_ = NetworkUtility.getLocalIPAddress()
|
|
let end = CFAbsoluteTimeGetCurrent()
|
|
timings.append(end - start)
|
|
}
|
|
|
|
// Calculate statistics
|
|
let average = timings.reduce(0, +) / Double(timings.count)
|
|
let max = timings.max() ?? 0
|
|
let min = timings.min() ?? 0
|
|
let stdDev = TestUtilities.calculateStandardDeviation(timings)
|
|
|
|
// Attach detailed performance metrics
|
|
Attachment.record("""
|
|
Iterations: \(iterations)
|
|
Average: \(String(format: "%.4f", average * 1_000))ms
|
|
Min: \(String(format: "%.4f", min * 1_000))ms
|
|
Max: \(String(format: "%.4f", max * 1_000))ms
|
|
Standard Deviation: \(String(format: "%.4f", stdDev * 1_000))ms
|
|
95th Percentile: \(String(format: "%.4f", calculatePercentile95(timings) * 1_000))ms
|
|
""", named: "Performance Metrics")
|
|
|
|
// Attach timing distribution for analysis
|
|
let timingData = timings.enumerated().map { i, timing in
|
|
"Iteration \(i + 1): \(String(format: "%.4f", timing * 1_000))ms"
|
|
}.joined(separator: "\n")
|
|
Attachment.record(timingData, named: "Individual Timings")
|
|
|
|
// Performance assertions
|
|
#expect(
|
|
average < 0.01,
|
|
"Average response time should be under 10ms, got \(String(format: "%.2f", average * 1_000))ms"
|
|
)
|
|
#expect(max < 0.05, "Maximum response time should be under 50ms, got \(String(format: "%.2f", max * 1_000))ms")
|
|
}
|
|
|
|
// MARK: - Concurrent Access Tests
|
|
|
|
@Test("Concurrent IP address retrieval", .tags(.concurrency))
|
|
func concurrentAccess() async throws {
|
|
await withTaskGroup(of: String?.self) { group in
|
|
// Multiple concurrent calls
|
|
for _ in 0..<10 {
|
|
group.addTask {
|
|
NetworkUtility.getLocalIPAddress()
|
|
}
|
|
}
|
|
|
|
var results: [String?] = []
|
|
for await result in group {
|
|
results.append(result)
|
|
}
|
|
|
|
// All calls should return the same value
|
|
let uniqueResults = Set(results.compactMap(\.self))
|
|
#expect(uniqueResults.count <= 1, "Concurrent calls returned different IPs")
|
|
}
|
|
}
|
|
|
|
// MARK: - Integration Tests
|
|
|
|
@Test("Network utility with system network state", .tags(.integration))
|
|
func systemNetworkState() throws {
|
|
let localIP = NetworkUtility.getLocalIPAddress()
|
|
let allIPs = NetworkUtility.getAllIPAddresses()
|
|
|
|
// If we have a local IP, it should be in the all IPs list
|
|
if let localIP {
|
|
#expect(allIPs.contains(localIP), "Local IP should be in all IPs list")
|
|
}
|
|
|
|
// All IPs should be unique
|
|
#expect(Set(allIPs).count == allIPs.count, "IP addresses should be unique")
|
|
}
|
|
|
|
@Test("IP address format consistency")
|
|
func iPAddressFormat() throws {
|
|
let allIPs = NetworkUtility.getAllIPAddresses()
|
|
|
|
for ip in allIPs {
|
|
// Should not have leading/trailing whitespace
|
|
#expect(ip == ip.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
|
|
// Should not contain port numbers
|
|
#expect(!ip.contains(":"))
|
|
|
|
// Should be standard dotted decimal notation
|
|
#expect(ip.contains("."))
|
|
}
|
|
}
|
|
|
|
// MARK: - Mock Tests
|
|
|
|
@Test("Mock network utility behavior")
|
|
@MainActor
|
|
func mockUtility() throws {
|
|
// Set up mock
|
|
MockNetworkUtility.mockLocalIP = "192.168.1.100"
|
|
MockNetworkUtility.mockAllIPs = ["192.168.1.100", "10.0.0.50"]
|
|
|
|
#expect(MockNetworkUtility.getLocalIPAddress() == "192.168.1.100")
|
|
#expect(MockNetworkUtility.getAllIPAddresses().count == 2)
|
|
|
|
// Test failure scenario
|
|
MockNetworkUtility.shouldFailGetAddresses = true
|
|
#expect(MockNetworkUtility.getLocalIPAddress() == nil)
|
|
#expect(MockNetworkUtility.getAllIPAddresses().isEmpty)
|
|
|
|
MockNetworkUtility.reset()
|
|
}
|
|
|
|
// MARK: - Helper Functions
|
|
|
|
/// Safely calculate 95th percentile, guarding against empty arrays and out-of-bounds access
|
|
private func calculatePercentile95(_ timings: [TimeInterval]) -> TimeInterval {
|
|
guard !timings.isEmpty else { return 0 }
|
|
let sortedTimings = timings.sorted()
|
|
let percentileIndex = min(Int(0.95 * Double(sortedTimings.count)), sortedTimings.count - 1)
|
|
return sortedTimings[percentileIndex]
|
|
}
|
|
}
|