105 lines
4.5 KiB
Swift
105 lines
4.5 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
@Suite @MainActor
|
|
struct TailscaleSessionRoutingTests {
|
|
@Test(arguments: [false, true])
|
|
func savedUsernameSkipsThePromptOnANewSession(sftp: Bool) async throws {
|
|
let network = try TailscaleNetwork(controlURL: TailscaleNetwork.defaultControlURL, dnsSuffix: "example.ts.net")
|
|
var group = HostGroup(name: "Tailnet")
|
|
group.tailscale = TailscaleGroupConfiguration(network: network)
|
|
let library = HostLibraryModel(groups: [group])
|
|
let provider = RoutingProvider()
|
|
library.tailscaleConnections = provider
|
|
library.replaceTailscalePeers(
|
|
[
|
|
TailscalePeer(
|
|
stableID: "n123", name: "Server",
|
|
dnsName: "server.example.ts.net", online: true, sshHostKeys: ["ssh-ed25519 fixture"])
|
|
], groupID: group.id)
|
|
let host = try #require(library.hosts.first)
|
|
#expect(library.saveTailscaleUsername("operator", hostID: host.id))
|
|
let terminal = sftp ? nil : SSHSessionModel(host: host)
|
|
let files = sftp ? SFTPChannelModel(hostID: host.id) : nil
|
|
defer {
|
|
terminal?.disconnect()
|
|
files?.disconnect()
|
|
}
|
|
terminal?.connect(using: library)
|
|
files?.connect(using: library)
|
|
#expect(terminal?.pendingAuthentication == nil)
|
|
#expect(files?.pendingAuthentication == nil)
|
|
for _ in 0..<200 {
|
|
if await provider.dialer.openedHost != nil { break }
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
#expect(await provider.dialer.openedHost == host.host)
|
|
}
|
|
|
|
@Test(arguments: [false, true])
|
|
func usernameSubmissionKeepsTerminalAndSFTPOnTheManagedTransport(sftp: Bool) async throws {
|
|
let network = try TailscaleNetwork(controlURL: TailscaleNetwork.defaultControlURL, dnsSuffix: "example.ts.net")
|
|
var group = HostGroup(name: "Tailnet")
|
|
group.tailscale = TailscaleGroupConfiguration(network: network)
|
|
let library = HostLibraryModel(groups: [group])
|
|
let provider = RoutingProvider()
|
|
library.tailscaleConnections = provider
|
|
library.replaceTailscalePeers(
|
|
[
|
|
TailscalePeer(
|
|
stableID: "n123", name: "Server", dnsName: "server.example.ts.net", online: true,
|
|
sshHostKeys: ["ssh-ed25519 fixture"])
|
|
], groupID: group.id)
|
|
let host = try #require(library.hosts.first)
|
|
let terminal = sftp ? nil : SSHSessionModel(host: host)
|
|
let files = sftp ? SFTPChannelModel(hostID: host.id) : nil
|
|
defer {
|
|
terminal?.disconnect()
|
|
files?.disconnect()
|
|
}
|
|
terminal?.connect(using: library)
|
|
files?.connect(using: library)
|
|
let prompt = try #require(terminal?.pendingAuthentication ?? files?.pendingAuthentication)
|
|
#expect(prompt.usernameOnly && prompt.username == "root")
|
|
terminal?.continueAuthentication(
|
|
identityID: nil, username: "operator", password: nil, passphrase: nil,
|
|
keyID: nil, save: true, using: library)
|
|
files?.continueAuthentication(
|
|
identityID: nil, username: "operator", password: nil, passphrase: nil,
|
|
keyID: nil, save: true, using: library)
|
|
for _ in 0..<200 {
|
|
if await provider.dialer.openedHost != nil { break }
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
#expect(await provider.dialer.openedHost == "server.example.ts.net")
|
|
#expect(await provider.dialer.requestedNodeIDs == ["n123"])
|
|
#expect(library.identities.isEmpty)
|
|
#expect(library.tailscaleUsername(for: host) == "operator")
|
|
}
|
|
}
|
|
|
|
@MainActor private final class RoutingProvider: TailscaleConnectionProviding {
|
|
let dialer = RoutingDialer()
|
|
func connection(for groupID: UUID) throws -> any SSHNetworkDialing { dialer }
|
|
}
|
|
|
|
private actor RoutingDialer: SSHNetworkDialing {
|
|
var openedHost: String?
|
|
var requestedNodeIDs: [String] = []
|
|
func open(host: String, port: Int, timeout: TimeInterval) async throws -> Int32 {
|
|
openedHost = host
|
|
throw CancellationError()
|
|
}
|
|
func hostKeys(stableNodeID: String) async throws -> Set<SSHHostKeyIdentity> {
|
|
requestedNodeIDs.append(stableNodeID)
|
|
return [SSHHostKeyIdentity(algorithm: .ed25519, fingerprint: "SHA256:tailnet")]
|
|
}
|
|
}
|