Files
Termind/FrameworkTests/LocalNetworkLifecycleTests.swift
T

184 lines
7.5 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import Foundation
import TermindApplication
import TermindDomain
import TermindRemoteInfrastructure
import Testing
import struct TermindDomain.Host
@Suite @MainActor
struct LocalNetworkLifecycleTests {
@Test(arguments: [LocalDiscoveryProtocol.ssh, .telnet])
func rescanningAndDismissalRejectLateDiscoveryResults(_ connectionProtocol: LocalDiscoveryProtocol) async throws {
let service = SuspendedDiscovery(connectionProtocol: connectionProtocol)
let model = LocalDiscoveryModel(service: service)
#expect(model.connectionProtocol == connectionProtocol)
model.scan()
await wait { service.requests.count == 1 }
#expect(service.requests.count == 1)
model.scan()
await wait { service.requests.count == 2 }
let current = DiscoveredLocalHost(
name: "Current", address: "192.168.1.5", port: connectionProtocol.port, source: .probe,
connectionProtocol: connectionProtocol)
let stale = DiscoveredLocalHost(
name: "Stale", address: "192.168.1.4", port: connectionProtocol.port, source: .probe,
connectionProtocol: connectionProtocol)
guard service.requests.count == 2 else {
service.finishAll()
return
}
service.requests[1].resume(returning: [current])
await wait { model.phase == .finished }
service.requests[0].resume(returning: [stale])
await Task.yield()
#expect(model.hosts == [current])
model.scan()
await wait { service.requests.count == 3 }
model.cancel()
if service.requests.count == 3 { service.requests[2].resume(returning: [stale]) }
await Task.yield()
#expect(model.hosts.isEmpty)
#expect(model.phase == .idle)
}
@Test func discoveryDistinguishesDeniedUnavailableFailureAndEmpty() async {
for (error, phase) in [
(LocalDiscoveryError.permissionDenied, LocalDiscoveryModel.Phase.permissionDenied),
(.unavailable, .unavailable), (.failed, .failed),
] {
let model = LocalDiscoveryModel(service: FailingDiscovery(error: error))
model.scan()
await wait { model.phase != .scanning }
#expect(model.phase == phase)
#expect(model.hosts.isEmpty)
}
}
@Test func macLookupUsesDraftCredentialsWithoutSavingOrAutoWaking() async throws {
let host = Host(name: "Unsaved", host: "draft.invalid")
let library = HostLibraryModel()
let resolver = RecordingMACResolver()
let model = HostMACAddressModel(
host: host, library: library, resolver: resolver,
submissions: [host.id: .init(username: "draft-user", password: "ephemeral")])
model.start()
await wait { model.isFinished }
#expect(model.isFinished)
#expect(library.hosts.isEmpty)
#expect(library.identities.isEmpty)
#expect(await resolver.receivedUsername == "draft-user")
#expect(await resolver.receivedHost == "draft.invalid")
#expect(await resolver.receivedWakeAction == false)
#expect(model.candidates.first?.address.rawValue == "02:11:22:33:44:55")
model.cancel()
}
@Test func physicalSSHAndTelnetOpenInvokeWakeBeforeNetworkAndRespectCancellation() async {
let counter = WakeCounter()
let before: ConnectionWakeAction = {
await counter.increment()
withUnsafeCurrentTask { $0?.cancel() }
}
let ssh = Task {
var config = SSHConnectionConfiguration(
hostID: UUID(), host: "never-dial.invalid", port: 22, username: "user", credentials: [],
expectedHostKeys: [])
config.beforeConnect = before
do {
try await SSHConnection().connect(using: config)
Issue.record("A cancelled physical connection must not open")
} catch {
#expect(error is CancellationError)
}
}
await ssh.value
#expect(await counter.count == 1)
let telnet = Task {
var config = TelnetConnectionConfiguration(host: "never-dial.invalid", port: 23)
config.beforeConnect = before
do {
try await TelnetConnection(output: { _ in }).connect(using: config)
Issue.record("A cancelled physical connection must not open")
} catch {
#expect(error is CancellationError)
}
}
await telnet.value
#expect(await counter.count == 2)
for transport in [HostTerminalProtocol.mosh, .eternalTerminal] {
let task = Task {
var config = SSHConnectionConfiguration(
hostID: UUID(), host: "never-dial.invalid", port: 22, username: "user", credentials: [],
expectedHostKeys: [])
config.beforeConnect = before
var options = HostTerminalTransports()
options.moshEnabled = true
options.eternalTerminalEnabled = true
let client = ResumableTerminalClient(
transport: transport, output: { _ in }, stateChanged: { _ in }, shellIntegration: { _ in })
do {
try await client.connect(
request: .init(configuration: config), options: options,
environment: [], shellIntegrationEnabled: false, columns: 80, rows: 24,
route: { _ in throw CancellationError() }, progress: nil)
Issue.record("A cancelled physical connection must not open")
} catch {
#expect(error is CancellationError)
}
await client.close()
}
await task.value
}
#expect(await counter.count == 4)
}
private func wait(until predicate: () -> Bool) async {
for _ in 0..<200 {
if predicate() { return }
try? await Task.sleep(for: .milliseconds(2))
}
}
}
@MainActor
private final class SuspendedDiscovery: LocalHostDiscovering {
let connectionProtocol: LocalDiscoveryProtocol
var requests: [CheckedContinuation<[DiscoveredLocalHost], any Error>] = []
init(connectionProtocol: LocalDiscoveryProtocol = .ssh) { self.connectionProtocol = connectionProtocol }
func discover() async throws -> [DiscoveredLocalHost] {
try await withCheckedThrowingContinuation { requests.append($0) }
}
func finishAll() { for request in requests { request.resume(returning: []) } }
}
@MainActor
private final class FailingDiscovery: LocalHostDiscovering {
let connectionProtocol = LocalDiscoveryProtocol.ssh
let error: LocalDiscoveryError
init(error: LocalDiscoveryError) { self.error = error }
func discover() async throws -> [DiscoveredLocalHost] { throw error }
}
private actor RecordingMACResolver: HostMACAddressResolving {
var receivedUsername: String?
var receivedHost: String?
var receivedWakeAction = false
func resolve(_ request: SSHConnectionRequest, progress: SSHRouteProgressHandler?) async throws
-> [HostMACAddressCandidate]
{
receivedUsername = request.configuration.username
receivedHost = request.configuration.host
receivedWakeAction = request.configuration.beforeConnect != nil
return [.init(interface: "en0", address: MACAddress("02:11:22:33:44:55")!)]
}
}
private actor WakeCounter {
var count = 0
func increment() { count += 1 }
}