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_ 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(); }