Files
Termind/TFMTests/SFTPChannelTests.swift

189 lines
6.9 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import Foundation
import TermindApplication
import TermindDomain
import TermindPresentation
import TermindRemoteInfrastructure
import Testing
import struct TermindDomain.Host
@testable import Termind
@Suite
@MainActor
struct SFTPChannelTests {
@Test
func workspaceSubtitleTracksSFTPPathEvenWhenRenamed() async throws {
let host = Host(name: "Files", host: "files.example")
let store = HostLibraryModel(hosts: [host])
let workspace = Workspace.sftpChannel(host: host, initialPath: "/srv")
let window = WindowModel(workspaces: [workspace])
let channel = try #require(workspace.focusedPane.sftpChannel)
try await channel.resolveInitialPathIfNeeded(using: LifecycleSFTPClient())
#expect(workspace.subtitle == "/srv")
window.rename(workspace.id, to: "Logs")
#expect(workspace.title(in: store) == "Logs")
#expect(workspace.subtitle == "/srv")
channel.navigate(to: "/var/log")
#expect(workspace.subtitle == "/var/log")
}
@Test
func workspaceWaitsForHomeThenUsesTilde() async throws {
let host = Host(name: "Files", host: "files.example")
let workspace = Workspace.sftpChannel(host: host, initialPath: "/home/test/project")
let channel = try #require(workspace.focusedPane.sftpChannel)
#expect(workspace.subtitle == nil)
try await channel.resolveInitialPathIfNeeded(using: LifecycleSFTPClient())
#expect(workspace.subtitle == "~/project")
}
@Test
func findFocusRequestsRepeatWithoutClearingTheQuery() throws {
let host = Host(name: "Files", host: "files.example")
let window = WindowModel(workspaces: [Workspace.sftpChannel(host: host)])
let channel = try #require(window.focusedSFTPChannel)
channel.query = "release"
window.focusToolbarFind()
window.focusToolbarFind()
#expect(window.findFocusRequest == 2)
#expect(channel.query == "release")
}
@Test
func navigationAndHistoryBelongToTheChannel() {
let sftpChannel = SFTPChannelModel(hostID: UUID(), initialPath: "/var/log/")
#expect(sftpChannel.path == "/var/log")
sftpChannel.navigate(to: "/etc")
sftpChannel.navigate(to: "/srv")
sftpChannel.goBack()
#expect(sftpChannel.path == "/etc")
#expect(sftpChannel.canGoForward)
sftpChannel.goForward()
#expect(sftpChannel.path == "/srv")
}
@Test
func movingAPaneKeepsTheSameSFTPChannelAndConnection() async {
let host = Host(name: "Files", host: "files.example")
let client = LifecycleSFTPClient()
let sftpChannel = SFTPChannelModel(
hostID: host.id, initialPath: "/", client: client
)
let source = Workspace(pane: Pane(.sftpChannel(sftpChannel)))
let target = Workspace.sshSession(host: host)
let pane = source.focusedPane
sftpChannel.navigate(to: "/srv/releases")
let window = WindowModel(workspaces: [source, target])
window.merge(
source.id, into: target.id, beside: target.focusedPane.id,
axis: .horizontal, side: .after
)
#expect(window.pane(pane.id) === pane)
guard case .sftpChannel(let moved)? = window.pane(pane.id)?.content else {
Issue.record("Moved browser was lost")
return
}
#expect(moved === sftpChannel)
#expect(moved.path == "/srv/releases")
await Task.yield()
#expect(await client.disconnectCount == 0)
}
@Test
func duplicateCreatesAnIndependentDefaultBrowser() {
let host = Host(name: "Files", host: "files.example")
let original = Pane.sftpChannel(host: host)
guard case .sftpChannel(let sftpChannel) = original.content else {
Issue.record("Expected an SFTP channel pane")
return
}
sftpChannel.navigate(to: "/srv/releases")
guard let copy = original.copy(), case .sftpChannel(let duplicate) = copy.content else {
Issue.record("Expected a duplicated SFTP channel pane")
return
}
#expect(duplicate !== sftpChannel)
#expect(duplicate.hostID == sftpChannel.hostID)
#expect(duplicate.path == "/")
duplicate.navigate(to: "/tmp")
#expect(sftpChannel.path == "/srv/releases")
}
@Test
func closingBrowserDoesNotRemoveGlobalTransfers() {
let host = Host(name: "Files", host: "files.example")
let sftpTransfer = SFTPTransfer(
hostID: host.id, localPath: "/tmp/archive.zip",
remotePath: "/srv/archive.zip"
)
let store = HostLibraryModel(hosts: [host], sftpTransfers: [sftpTransfer])
let workspace = Workspace.sftpChannel(host: host)
let window = WindowModel(workspaces: [workspace])
window.closePane(workspace.focusedPane.id)
#expect(window.workspaces.isEmpty)
#expect(store.sftpTransfers == [sftpTransfer])
}
@Test
func closingBrowserDisconnectsItsBrowsingConnection() async {
let host = Host(name: "Files", host: "files.example")
let client = LifecycleSFTPClient()
let sftpChannel = SFTPChannelModel(
hostID: host.id, initialPath: "/", client: client
)
let workspace = Workspace(pane: Pane(.sftpChannel(sftpChannel)))
let window = WindowModel(workspaces: [workspace])
window.closePane(workspace.focusedPane.id)
while await client.disconnectCount == 0 { await Task.yield() }
#expect(await client.disconnectCount == 1)
#expect(sftpChannel.status == .disconnected)
}
}
private actor LifecycleSFTPClient: SFTPClientProtocol {
private(set) var disconnectCount = 0
func connect(
using configuration: SSHConnectionConfiguration,
progress: SSHRouteProgressHandler?
) async throws {}
func disconnect() async { disconnectCount += 1 }
func homeDirectory() async throws -> String { "/home/test" }
func listDirectory(at path: String) async throws -> [RemoteFile] { [] }
func attributes(at path: String) async throws -> RemoteFile? { nil }
func setPermissions(at path: String, to permissions: UInt32) async throws {}
func createDirectory(at path: String) async throws {}
func rename(from source: String, to destination: String, overwrite: Bool) async throws {}
func removeFile(at path: String) async throws {}
func removeDirectory(at path: String) async throws {}
func uploadFile(
from localURL: URL, to remotePath: String, overwrite: Bool,
cancellation: TransferCancellationToken,
progress: @escaping @Sendable (SFTPProgress) -> Void
) async throws {}
func downloadFile(
from remotePath: String, to localURL: URL, overwrite: Bool,
cancellation: TransferCancellationToken,
progress: @escaping @Sendable (SFTPProgress) -> Void
) async throws {}
}