From ab8faf5877f17a0bd28d25d7212ae436b224fc6c Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:53:08 +0800 Subject: [PATCH] test(sftp): drive the subsystem against a real server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SftpManager` had no live coverage of any kind. Its tests cover the path arithmetic — `remote_join`, `remote_parent`, `safe_local_name` — and the transport-failure classifier, none of which needs a server, and all of which sits downstream of a subsystem no test had ever opened. The whole panel is built on two calls, `list` and `op`, and neither had been made against sshd. Now both are, and I ran it: mkdir a scratch directory, create a file in it, list it and check the entry comes back as a `File`, rename it and check both that the new name is there and the old one is not, stat a path that was never created and require the answer to be something other than `Done`, then remove the file, remove the directory, and confirm listing it fails. Everything lives under a directory named for the process on the far side and is removed at the end; the far side being this same machine is what makes it safe to assert about. Checked afterwards that `$HOME` holds no leftovers. Takes its turn on the same mutex as the other two, for the reason that one documents: they share `SshManager::global()`. All three pass together — the fourth, GSSAPI, still wants a Kerberos realm this machine does not have. --- crates/tty7-core/src/daemon/ssh/mod.rs | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/crates/tty7-core/src/daemon/ssh/mod.rs b/crates/tty7-core/src/daemon/ssh/mod.rs index 5336c72d..17287f82 100644 --- a/crates/tty7-core/src/daemon/ssh/mod.rs +++ b/crates/tty7-core/src/daemon/ssh/mod.rs @@ -1047,6 +1047,95 @@ mod tests { }); } + /// The SFTP subsystem against a real server: create, list, stat, rename, + /// remove. + /// + /// `SftpManager` had no live coverage of any kind. Its own tests cover the + /// path arithmetic — `remote_join`, `remote_parent`, `safe_local_name` — + /// and the transport-failure classifier, none of which needs a server, and + /// all of which is downstream of a subsystem that was never opened in a + /// test. The panel is built entirely on these two calls. + /// + /// Everything happens under a scratch directory named for this process, on + /// the far side, and is removed at the end. The far side being this same + /// machine is what makes that safe to assert about. + #[test] + #[ignore = "requires a live SSH server that accepts a key of yours"] + fn live_sftp_lists_and_edits_a_real_directory() { + let _turn = live_ssh_turn(); + let spec = live_key_spec(); + let manager = SshManager::global(); + let broker = PromptBroker::new(Box::new(|_| true)); + let conn = manager + .runtime + .block_on(async { manager.open_connection(&spec, &broker).await }) + .expect("a connection to run sftp over") + .0; + + let sftp = crate::daemon::ssh::sftp::SftpManager::global(); + let root = format!( + "{}/tty7-sftp-live-{}", + std::env::var("HOME").unwrap_or_else(|_| "/tmp".into()), + std::process::id() + ); + let file = format!("{root}/hello.txt"); + let moved = format!("{root}/moved.txt"); + + use crate::daemon::protocol::{SftpEntryKind, SftpOp, SftpOpResult}; + let ok = |op: SftpOp| match sftp.op(&conn, &op) { + SftpOpResult::Done => {} + other => panic!("{op:?} answered {other:?}"), + }; + + ok(SftpOp::Mkdir { path: root.clone() }); + ok(SftpOp::CreateFile { path: file.clone() }); + + let listed = sftp.list(&conn, &root).expect("list the scratch directory"); + let hello = listed + .iter() + .find(|e| e.name == "hello.txt") + .unwrap_or_else(|| panic!("hello.txt is missing from {listed:?}")); + assert_eq!(hello.kind, SftpEntryKind::File, "and it is a file"); + + ok(SftpOp::Rename { + from: file.clone(), + to: moved.clone(), + }); + let listed = sftp.list(&conn, &root).expect("list it again"); + assert!( + listed.iter().any(|e| e.name == "moved.txt"), + "the rename moved it: {listed:?}" + ); + assert!( + !listed.iter().any(|e| e.name == "hello.txt"), + "and left nothing behind: {listed:?}" + ); + + // A path that is not there answers rather than hanging or succeeding. + assert!( + !matches!( + sftp.op( + &conn, + &SftpOp::Stat { + path: format!("{root}/never-existed"), + } + ), + SftpOpResult::Done + ), + "stat of a missing path is not Done" + ); + + ok(SftpOp::RemoveFile { path: moved }); + ok(SftpOp::RemoveDir { path: root.clone() }); + assert!( + sftp.list(&conn, &root).is_err(), + "the scratch directory is gone" + ); + + conn.mark_dead(); + manager.evict_connection(conn.key()); + } + #[test] #[ignore = "requires a live SSH server and local GSSAPI credentials"] fn live_gssapi_connects_and_opens_a_channel() {