vibetunnel/ios/VibeTunnelTests/Services/BonjourDiscoveryServiceTests.swift
Igor Tarasenko 9fad6301a0
feat: Add Bonjour/mDNS service discovery for iOS app (#226)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2025-07-05 11:34:36 +01:00

188 lines
6.7 KiB
Swift

import Foundation
import Network
import Testing
@testable import VibeTunnel
@Suite("BonjourDiscoveryService Tests")
struct BonjourDiscoveryServiceTests {
@Suite("Core Functionality")
struct CoreFunctionality {
@Test("Start discovery initiates service properly")
@MainActor
func testStartDiscovery() async {
// Given
let service = BonjourDiscoveryService.shared
service.stopDiscovery() // Ensure clean state
// When
service.startDiscovery()
// Allow time for service to initialize
// Network browser state changes are asynchronous
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
// Then - In test environment, the browser may not reach "ready" state
// without actual network discovery, so we just verify the service
// can be started without crashing
// The service creates a browser internally when startDiscovery is called
// Cleanup
service.stopDiscovery()
#expect(service.isDiscovering == false)
}
@Test("Stop discovery cancels and clears state")
@MainActor
func testStopDiscovery() async {
// Given
let service = BonjourDiscoveryService.shared
service.startDiscovery()
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
// When
service.stopDiscovery()
// Then
#expect(service.isDiscovering == false)
// Note: The service doesn't clear discoveredServers on stop,
// it only clears them on the next startDiscovery call
}
@Test("Service handles discovery state correctly")
@MainActor
func discoveryStateManagement() async {
// Given
let service = BonjourDiscoveryService.shared
service.stopDiscovery() // Ensure clean state
// When - Start discovery
service.startDiscovery()
// Allow time for discovery to start
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
// Then - Verify service can manage discovery state
// Note: In test environment without actual mDNS services,
// we just verify the service can start discovery mode
let initialServerCount = service.discoveredServers.count
// Cleanup
service.stopDiscovery()
// Verify discovery was stopped
#expect(service.isDiscovering == false)
}
}
@Suite("Service Resolution")
struct ServiceResolution {
@Test("Resolve service can handle server data")
@MainActor
func resolveService() async {
// Given
let service = BonjourDiscoveryService.shared
let testServer = DiscoveredServer(
name: "TestServer",
host: "192.168.1.100",
port: 4_020,
metadata: [:]
)
// When - Test that DiscoveredServer structure works correctly
let serverId = testServer.id
let displayName = testServer.displayName
// Then - Verify server data is correctly structured
#expect(!serverId.uuidString.isEmpty)
#expect(displayName == "TestServer")
#expect(testServer.host == "192.168.1.100")
#expect(testServer.port == 4_020)
}
@Test("Resolve service handles IPv6 addresses correctly")
@MainActor
func resolveServiceIPv6() async {
// Given
let ipv6Host = "fe80::1%en0" // IPv6 with interface
// Simulate resolution with IPv6
let testServer = DiscoveredServer(
name: "IPv6Server",
host: ipv6Host,
port: 4_020,
metadata: [:]
)
// When processing, the service should strip the interface
let cleanedHost = ipv6Host.components(separatedBy: "%").first ?? ipv6Host
// Then
#expect(cleanedHost == "fe80::1")
#expect(testServer.host == ipv6Host) // Original host preserved
}
@Test("Resolve service uses ID-based lookup to avoid race conditions")
@MainActor
func resolveServiceRaceCondition() async {
// Given
let server1 = DiscoveredServer(name: "Server1", host: "192.168.1.1", port: 4_020, metadata: [:])
let server2 = DiscoveredServer(name: "Server2", host: "192.168.1.2", port: 4_020, metadata: [:])
let server3 = DiscoveredServer(name: "Server3", host: "192.168.1.3", port: 4_020, metadata: [:])
// Create a test array simulating discovered servers
let servers = [server1, server2, server3]
let originalId = server2.id
// Simulate removal of server2 (race condition scenario)
let updatedServers = [server1, server3]
// Then - Verify ID-based lookup handles race condition correctly
let foundInOriginal = servers.firstIndex(where: { $0.id == originalId })
let foundInUpdated = updatedServers.firstIndex(where: { $0.id == originalId })
#expect(foundInOriginal != nil) // Server was in original list
#expect(foundInUpdated == nil) // Server correctly not found after removal
// Verify remaining servers are still accessible by ID
#expect(updatedServers.contains(where: { $0.id == server1.id }))
#expect(updatedServers.contains(where: { $0.id == server3.id }))
}
}
@Suite("Error Handling")
struct ErrorHandling {
@Test("Service handles empty browse results")
@MainActor
func emptyBrowseResults() async {
// Given
let service = BonjourDiscoveryService.shared
// When - Start and stop discovery
service.startDiscovery()
service.stopDiscovery()
// Then - Service should handle empty results gracefully
#expect(service.isDiscovering == false)
}
@Test("Multiple start calls are idempotent")
@MainActor
func multipleStartCalls() async {
// Given
let service = BonjourDiscoveryService.shared
service.stopDiscovery() // Clean state
// When
service.startDiscovery()
let firstState = service.isDiscovering
service.startDiscovery() // Second call
service.startDiscovery() // Third call
// Then
#expect(firstState == service.isDiscovering)
// Cleanup
service.stopDiscovery()
}
}
}