112 lines
5.0 KiB
Swift
112 lines
5.0 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import Testing
|
|
|
|
import struct TermindDomain.Host
|
|
|
|
@testable import Termind
|
|
|
|
@Suite @MainActor
|
|
struct RemoteSessionConnectionControllerTests {
|
|
@Test(arguments: [ConnectionPurpose.sshSession, .sftpChannel])
|
|
func reconnectTearsDownBeforeJourneyAndRetriesWithTheStore(_ purpose: ConnectionPurpose) async {
|
|
let jump = Host(name: "Jump", host: "jump.invalid")
|
|
let host = Host(name: "Server", host: "server.invalid")
|
|
let store = HostLibraryModel(hosts: [host])
|
|
let controller = RemoteSessionConnectionController(
|
|
reconnect: SessionReconnectController(sleep: { _ in try await Task.sleep(for: .seconds(3_600)) }))
|
|
let route = SSHConnectionRoute(hosts: [jump, host], usernames: [host.id: "route-user"])
|
|
controller.connectionRoute = route
|
|
controller.markConnected()
|
|
var didTeardown = false
|
|
var didRetry = false
|
|
|
|
controller.scheduleReconnect(
|
|
after: "offline", hostID: host.id, using: store, purpose: purpose,
|
|
route: purpose == .sshSession ? route : nil
|
|
) {
|
|
#expect(controller.journey.phase == .hidden)
|
|
#expect(controller.reconnectState == nil)
|
|
didTeardown = true
|
|
} retry: { retryStore in
|
|
#expect(didTeardown)
|
|
#expect(retryStore === store)
|
|
#expect(controller.journey.mode == .reconnection)
|
|
didRetry = true
|
|
}
|
|
|
|
#expect(didTeardown)
|
|
#expect(controller.reconnectState?.lastError == "offline")
|
|
#expect(controller.journey.purpose == purpose)
|
|
#expect(
|
|
controller.journey.endpoints.compactMap(\.hostID)
|
|
== (purpose == .sshSession ? [jump.id, host.id] : [host.id]))
|
|
controller.reconnect.retryNow()
|
|
for _ in 0..<100 where !didRetry { await Task.yield() }
|
|
#expect(didRetry)
|
|
controller.disconnect()
|
|
}
|
|
|
|
@Test func disconnectCancelsTheScheduledReconnect() async {
|
|
let host = Host(name: "Server", host: "server.invalid")
|
|
let store = HostLibraryModel(hosts: [host])
|
|
let controller = RemoteSessionConnectionController(
|
|
reconnect: SessionReconnectController(sleep: { _ in try await Task.sleep(for: .seconds(3_600)) }))
|
|
controller.markConnected()
|
|
var didRetry = false
|
|
controller.scheduleReconnect(
|
|
after: "offline", hostID: host.id, using: store, purpose: .sftpChannel,
|
|
teardown: {}, retry: { _ in didRetry = true })
|
|
controller.disconnect()
|
|
controller.reconnect.retryNow()
|
|
for _ in 0..<100 { await Task.yield() }
|
|
#expect(!didRetry)
|
|
#expect(controller.reconnectState == nil)
|
|
#expect(controller.journey.phase == .hidden)
|
|
}
|
|
|
|
@Test func hostResolutionPrefersRouteThenLibraryThenTemporaryDestination() {
|
|
let destination = Host(name: "Library", host: "library.invalid")
|
|
var routed = destination
|
|
routed.host = "route.invalid"
|
|
let temporary = Host(name: "Temporary", host: "temporary.invalid")
|
|
let store = HostLibraryModel(hosts: [destination])
|
|
let controller = RemoteSessionConnectionController()
|
|
controller.transientHost = temporary
|
|
controller.connectionRoute = SSHConnectionRoute(hosts: [routed])
|
|
|
|
#expect(controller.connectionHost(destination.id, destinationID: temporary.id, using: store) == routed)
|
|
controller.connectionRoute = nil
|
|
#expect(controller.connectionHost(destination.id, destinationID: temporary.id, using: store) == destination)
|
|
#expect(controller.connectionHost(temporary.id, destinationID: temporary.id, using: store) == temporary)
|
|
#expect(controller.connectionHost(UUID(), destinationID: temporary.id, using: store) == nil)
|
|
}
|
|
|
|
@Test func progressIgnoresReplacedAndClearedAttempts() async {
|
|
let host = Host(name: "Server", host: "server.invalid")
|
|
let controller = RemoteSessionConnectionController()
|
|
controller.journey.begin(host: host, route: [host], purpose: .sshSession, username: "deploy")
|
|
let first = UUID()
|
|
controller.connectionAttemptID = first
|
|
let oldProgress = controller.progressHandler(attemptID: first)
|
|
let current = UUID()
|
|
controller.connectionAttemptID = current
|
|
let progress = controller.progressHandler(attemptID: current)
|
|
let initialCount = controller.journey.logs.count
|
|
oldProgress(.initializing(host.id, "stale"))
|
|
progress(.initializing(host.id, "current"))
|
|
for _ in 0..<100 where controller.journey.logs.count == initialCount { await Task.yield() }
|
|
#expect(controller.journey.logs.last?.message == "current")
|
|
#expect(controller.journey.logs.count == initialCount + 1)
|
|
|
|
controller.connectionAttemptID = nil
|
|
progress(.initializing(host.id, "cleared"))
|
|
for _ in 0..<100 { await Task.yield() }
|
|
#expect(controller.journey.logs.count == initialCount + 1)
|
|
}
|
|
}
|