P0: exit-status for subsystem, improved error msgs, integration test suite
This commit is contained in:
249
markbase-core/tests/integration_test.rs
Normal file
249
markbase-core/tests/integration_test.rs
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user