test(sftp): drive the subsystem against a real server

`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.
This commit is contained in:
l0ng-ai
2026-08-23 08:53:08 +08:00
parent 510a42c2e5
commit ab8faf5877
+89
View File
@@ -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() {