P0: exit-status for subsystem, improved error msgs, integration test suite
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

This commit is contained in:
Warren
2026-06-20 16:40:29 +08:00
parent 5b439dfbef
commit 45d050c0b3
2 changed files with 260 additions and 3 deletions

View File

@@ -0,0 +1,249 @@
use std::path::PathBuf;
use std::process::{Child, Command, Output};
use std::thread::sleep;
use std::time::{Duration, Instant};
/// Find a free TCP port by binding to port 0
fn find_free_port() -> u16 {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
}
fn get_binary_path() -> PathBuf {
// For integration tests, cargo builds the binary at CARGO_BIN_EXE_<name>
if let Ok(path) = std::env::var("CARGO_BIN_EXE_markbase-core") {
return PathBuf::from(path);
}
// Fallback: look relative to manifest dir
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.expect("CARGO_MANIFEST_DIR not set");
let candidate = PathBuf::from(&manifest_dir)
.parent()
.expect("No parent of manifest dir")
.join("target")
.join("debug")
.join("markbase-core");
if candidate.exists() {
return candidate;
}
panic!("Cannot find markbase-core binary at {:?}. Build with: cargo build -p markbase-core", candidate);
}
fn start_ssh_server(port: u16) -> Child {
let binary = get_binary_path();
let log_file = std::env::temp_dir().join(format!("ssh_server_{}.log", port));
let log_file_handle = std::fs::File::create(&log_file)
.expect("Failed to create server log file");
eprintln!("SSH server log: {:?}", log_file);
// Server uses relative path "data/auth.sqlite" from project root
let project_root = std::env::var("CARGO_MANIFEST_DIR")
.map(|d| PathBuf::from(d).parent().unwrap().to_path_buf())
.unwrap_or_else(|_| PathBuf::from("."));
let child = Command::new(&binary)
.args(["ssh-start", "--port", &port.to_string()])
.current_dir(&project_root)
.env("RUST_LOG", "info")
.stdout(log_file_handle.try_clone().unwrap())
.stderr(log_file_handle)
.spawn()
.expect("Failed to start SSH server");
child
}
fn wait_for_port(port: u16, timeout: Duration) -> bool {
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
let start = Instant::now();
while start.elapsed() < timeout {
match std::net::TcpStream::connect_timeout(&addr, Duration::from_millis(1000)) {
Ok(stream) => {
// Connection accepted — verify server is ready by trying a small read
let _ = stream.set_read_timeout(Some(Duration::from_millis(200)));
return true;
}
Err(_) => {
sleep(Duration::from_millis(300));
}
}
}
false
}
fn run_ssh(port: u16, cmd: &str) -> Output {
let port_str = port.to_string();
let host = format!("demo@127.0.0.1");
let output = Command::new("sshpass")
.args(["-p", "demo123", "ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "PreferredAuthentications=password",
"-p", &port_str,
&host,
cmd])
.output();
match output {
Ok(o) => o,
Err(e) => {
panic!("Failed to run ssh command: {} (is sshpass installed?)", e);
}
}
}
fn run_scp(port: u16, src: &str, dst: &str, legacy: bool) -> Output {
let port_str = port.to_string();
let mut args = vec!["-P", &port_str];
if legacy {
args.push("-O");
}
args.extend_from_slice(&[
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
src,
dst,
]);
let output = if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
let mut sshpass_args = vec!["-p", "demo123", "scp", "-P", &port_str];
if legacy {
sshpass_args.push("-O");
}
sshpass_args.extend_from_slice(&[
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
src,
dst,
]);
Command::new("sshpass").args(&sshpass_args).output()
} else {
Command::new("scp").args(&args).output()
};
match output {
Ok(o) => o,
Err(e) => {
panic!("Failed to run scp command: {} (is sshpass installed?)", e);
}
}
}
/// Test SSH exec with exit code
#[test]
#[ignore = "requires sshpass and a running SSH server"]
fn test_ssh_exec_exit_code() {
let port = find_free_port();
let mut server = start_ssh_server(port);
assert!(wait_for_port(port, Duration::from_secs(15)), "Server failed to start");
// Test exit code 42
let output = run_ssh(port, "exit 42");
assert_eq!(output.status.code(), Some(42), "Expected exit code 42, got {:?}", output.status.code());
// Test exit code 0 (default)
let output = run_ssh(port, "echo hello");
assert_eq!(output.status.code(), Some(0), "Expected exit code 0, got {:?}", output.status.code());
assert!(String::from_utf8_lossy(&output.stdout).contains("hello"), "Expected hello in output");
let _ = server.kill();
let _ = server.wait();
}
/// Test SCP legacy file transfer
#[test]
#[ignore = "requires sshpass and a running SSH server"]
fn test_scp_legacy_transfer() {
let port = find_free_port();
let mut server = start_ssh_server(port);
assert!(wait_for_port(port, Duration::from_secs(15)), "Server failed to start");
let tmp_dir = std::env::temp_dir();
let src_path = tmp_dir.join("scp_legacy_src.txt");
let content = "legacy scp test data\nline 2\n";
std::fs::write(&src_path, content).unwrap();
let dst = format!("demo@127.0.0.1:scp_legacy_dst.txt");
let output = run_scp(port, src_path.to_str().unwrap(), &dst, true);
assert!(output.status.success(), "SCP legacy failed: {}", String::from_utf8_lossy(&output.stderr));
// Verify file content on server
let home = PathBuf::from("/Users/accusys/momentry/var/sftpgo/data/demo");
let dst_path = home.join("scp_legacy_dst.txt");
let result = std::fs::read_to_string(&dst_path);
assert!(result.is_ok(), "Remote file not found: {:?}", dst_path);
assert_eq!(result.unwrap(), content, "File content mismatch");
let _ = std::fs::remove_file(&dst_path);
let _ = server.kill();
let _ = server.wait();
}
/// Test SCP modern (SFTP protocol) file transfer
#[test]
#[ignore = "requires sshpass and a running SSH server"]
fn test_scp_modern_transfer() {
let port = find_free_port();
let mut server = start_ssh_server(port);
assert!(wait_for_port(port, Duration::from_secs(15)), "Server failed to start");
let tmp_dir = std::env::temp_dir();
let src_path = tmp_dir.join("scp_modern_src.txt");
let content = "modern scp test data\nanother line\nand another\n";
std::fs::write(&src_path, content).unwrap();
let dst = format!("demo@127.0.0.1:scp_modern_dst.txt");
let output = run_scp(port, src_path.to_str().unwrap(), &dst, false);
assert!(output.status.success(), "SCP modern failed: {}", String::from_utf8_lossy(&output.stderr));
// Verify file content on server
let home = PathBuf::from("/Users/accusys/momentry/var/sftpgo/data/demo");
let dst_path = home.join("scp_modern_dst.txt");
let result = std::fs::read_to_string(&dst_path);
assert!(result.is_ok(), "Remote file not found: {:?}", dst_path);
assert_eq!(result.unwrap(), content, "File content mismatch");
let _ = std::fs::remove_file(&dst_path);
let _ = server.kill();
let _ = server.wait();
}
/// Test both SCP modes + exec in sequence
#[test]
#[ignore = "requires sshpass and a running SSH server"]
fn test_full_workflow() {
let port = find_free_port();
let mut server = start_ssh_server(port);
assert!(wait_for_port(port, Duration::from_secs(15)), "Server failed to start");
// 1. Exec test
let output = run_ssh(port, "uname -a");
assert!(output.status.success(), "Exec test failed");
// 2. Legacy SCP
let tmp_dir = std::env::temp_dir();
let src_path = tmp_dir.join("wf_scp_src.txt");
std::fs::write(&src_path, "full workflow test").unwrap();
let dst = format!("demo@127.0.0.1:wf_scp_dst.txt");
let output = run_scp(port, src_path.to_str().unwrap(), &dst, true);
assert!(output.status.success(), "Legacy SCP in workflow failed");
// 3. Modern SCP
let src_path2 = tmp_dir.join("wf_scp_modern_src.txt");
std::fs::write(&src_path2, "modern workflow test").unwrap();
let dst2 = format!("demo@127.0.0.1:wf_scp_modern_dst.txt");
let output = run_scp(port, src_path2.to_str().unwrap(), &dst2, false);
assert!(output.status.success(), "Modern SCP in workflow failed");
// 4. Verify files
let home = PathBuf::from("/Users/accusys/momentry/var/sftpgo/data/demo");
for f in ["wf_scp_dst.txt", "wf_scp_modern_dst.txt"] {
let p = home.join(f);
assert!(p.exists(), "File {} not found", f);
let _ = std::fs::remove_file(&p);
}
let _ = server.kill();
let _ = server.wait();
}