use std::fs;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
const FEDORA_HOST: &str = "fedora";
const FEDORA_USER: &str = "nick";
fn create_fedora_config() -> msy::ssh::config::SshConfig {
use msy::ssh::config::SshConfig;
let mut config = SshConfig::new(FEDORA_HOST);
config.user = FEDORA_USER.to_string();
config.port = 22;
config
}
fn create_remote_test_path(test_name: &str) -> String {
format!("/tmp/sy_ssh_test_{}_{}", test_name, std::process::id())
}
fn cleanup_remote_path(path: &str) {
let cleanup_cmd = format!("ssh {} 'rm -rf {}'", FEDORA_HOST, path);
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(&cleanup_cmd)
.output();
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_scan_directory() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("scan");
let setup_cmd = format!(
"ssh {} 'mkdir -p {}/subdir && echo test1 > {}/file1.txt && echo test2 > {}/subdir/file2.txt && touch {}/empty.txt'",
FEDORA_HOST, remote_path, remote_path, remote_path, remote_path
);
std::process::Command::new("sh")
.arg("-c")
.arg(&setup_cmd)
.output()
.expect("Failed to create remote test structure");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let entries = transport
.scan(std::path::Path::new(&remote_path))
.await
.expect("Scan failed");
assert!(
entries.len() >= 4,
"Expected at least 4 entries, got {}",
entries.len()
);
let paths: Vec<String> = entries
.iter()
.map(|e| e.path.to_string_lossy().to_string())
.collect();
assert!(
paths.iter().any(|p| p.contains("file1.txt")),
"Missing file1.txt"
);
assert!(
paths.iter().any(|p| p.contains("file2.txt")),
"Missing file2.txt"
);
assert!(
paths.iter().any(|p| p.contains("empty.txt")),
"Missing empty.txt"
);
assert!(paths.iter().any(|p| p.contains("subdir")), "Missing subdir");
cleanup_remote_path(&remote_path);
println!("✅ SSH scan_directory: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_exists() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("exists");
let setup_cmd = format!("ssh {} 'echo test > {}'", FEDORA_HOST, remote_path);
std::process::Command::new("sh")
.arg("-c")
.arg(&setup_cmd)
.output()
.expect("Failed to create remote file");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let exists = transport
.exists(std::path::Path::new(&remote_path))
.await
.expect("exists() failed");
assert!(exists, "File should exist");
let nonexistent = format!("{}_nonexistent", remote_path);
let exists = transport
.exists(std::path::Path::new(&nonexistent))
.await
.expect("exists() failed");
assert!(!exists, "Nonexistent file should not exist");
cleanup_remote_path(&remote_path);
println!("✅ SSH exists: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_create_dir_all() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("create_dir");
let nested_path = format!("{}/a/b/c/d", remote_path);
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
transport
.create_dir_all(std::path::Path::new(&nested_path))
.await
.expect("create_dir_all failed");
let exists = transport
.exists(std::path::Path::new(&nested_path))
.await
.expect("exists check failed");
assert!(exists, "Nested directory should exist");
cleanup_remote_path(&remote_path);
println!("✅ SSH create_dir_all: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_read_write_file() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("read_write");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let test_data = b"Hello from SSH write_file test!\nLine 2\nLine 3";
transport
.write_file(
std::path::Path::new(&remote_path),
test_data,
SystemTime::now(),
)
.await
.expect("write_file failed");
let read_data = transport
.read_file(std::path::Path::new(&remote_path))
.await
.expect("read_file failed");
assert_eq!(read_data, test_data, "Read data doesn't match written data");
cleanup_remote_path(&remote_path);
println!("✅ SSH read_write_file: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
#[serial_test::serial]
async fn test_ssh_remove() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("remove");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
transport
.write_file(
std::path::Path::new(&remote_path),
b"test",
SystemTime::now(),
)
.await
.expect("write_file failed");
let exists = transport
.exists(std::path::Path::new(&remote_path))
.await
.expect("exists check failed");
assert!(exists, "File should exist before removal");
transport
.remove(std::path::Path::new(&remote_path), false)
.await
.expect("remove failed");
let exists = transport
.exists(std::path::Path::new(&remote_path))
.await
.expect("exists check failed");
assert!(!exists, "File should not exist after removal");
println!("✅ SSH remove: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_get_mtime() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("mtime");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let known_time = SystemTime::now() - Duration::from_secs(3600); transport
.write_file(std::path::Path::new(&remote_path), b"test", known_time)
.await
.expect("write_file failed");
let mtime = transport
.get_mtime(std::path::Path::new(&remote_path))
.await
.expect("get_mtime failed");
let diff = mtime
.duration_since(known_time)
.unwrap_or_else(|e| e.duration());
assert!(
diff < Duration::from_secs(2),
"Mtime difference too large: {:?}",
diff
);
cleanup_remote_path(&remote_path);
println!("✅ SSH get_mtime: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
#[serial_test::serial]
async fn test_ssh_file_info() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("file_info");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let test_data = b"test file info content";
transport
.write_file(
std::path::Path::new(&remote_path),
test_data,
SystemTime::now(),
)
.await
.expect("write_file failed");
let info = transport
.file_info(std::path::Path::new(&remote_path))
.await
.expect("file_info failed");
assert_eq!(info.size, test_data.len() as u64, "File size mismatch");
cleanup_remote_path(&remote_path);
println!("✅ SSH file_info: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_copy_file_basic() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_source = create_remote_test_path("copy_source");
let remote_dest = create_remote_test_path("copy_dest");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let test_data = vec![0x42u8; 1024 * 1024]; transport
.write_file(
std::path::Path::new(&remote_source),
&test_data,
SystemTime::now(),
)
.await
.expect("write_file failed");
let local_temp = TempDir::new().unwrap();
let local_file = local_temp.path().join("temp.dat");
let result = transport
.copy_file_streaming(std::path::Path::new(&remote_source), &local_file, None)
.await
.expect("copy_file_streaming failed");
assert_eq!(result.bytes_written, 1024 * 1024, "Bytes written mismatch");
let read_data = fs::read(&local_file).expect("Failed to read local file");
assert_eq!(read_data, test_data, "Copied data doesn't match");
cleanup_remote_path(&remote_source);
cleanup_remote_path(&remote_dest);
println!("✅ SSH copy_file_basic: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_copy_file_with_progress() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_source = create_remote_test_path("copy_progress");
let create_cmd = format!(
"ssh {} 'dd if=/dev/zero of={} bs=1M count=5 2>/dev/null'",
FEDORA_HOST, remote_source
);
std::process::Command::new("sh")
.arg("-c")
.arg(&create_cmd)
.output()
.expect("Failed to create remote file");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let local_dest = TempDir::new().unwrap().path().join("downloaded.dat");
let updates = Arc::new(AtomicU64::new(0));
let updates_clone = updates.clone();
let last_bytes = Arc::new(AtomicU64::new(0));
let last_bytes_clone = last_bytes.clone();
let progress_callback = Arc::new(move |bytes: u64, total: u64| {
updates_clone.fetch_add(1, Ordering::SeqCst);
last_bytes_clone.store(bytes, Ordering::SeqCst);
assert!(bytes <= total, "bytes should not exceed total");
});
let result = transport
.copy_file_streaming(
std::path::Path::new(&remote_source),
&local_dest,
Some(progress_callback),
)
.await
.expect("Download failed");
assert_eq!(result.bytes_written, 5 * 1024 * 1024);
let update_count = updates.load(Ordering::SeqCst);
assert!(
update_count >= 5,
"Expected >= 5 updates, got {}",
update_count
);
let final_bytes = last_bytes.load(Ordering::SeqCst);
assert_eq!(final_bytes, 5 * 1024 * 1024);
cleanup_remote_path(&remote_source);
println!("✅ SSH copy_file_with_progress: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_copy_empty_file() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_source = create_remote_test_path("empty_source");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
transport
.write_file(std::path::Path::new(&remote_source), b"", SystemTime::now())
.await
.expect("write_file failed");
let local_dest = TempDir::new().unwrap().path().join("empty.dat");
let result = transport
.copy_file_streaming(std::path::Path::new(&remote_source), &local_dest, None)
.await
.expect("Copy failed");
assert_eq!(result.bytes_written, 0, "Empty file should have 0 bytes");
assert!(local_dest.exists(), "Empty file should exist locally");
cleanup_remote_path(&remote_source);
println!("✅ SSH copy_empty_file: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_create_symlink() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_base = create_remote_test_path("symlink");
let remote_target = format!("{}/target.txt", remote_base);
let remote_link = format!("{}/link.txt", remote_base);
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
transport
.create_dir_all(std::path::Path::new(&remote_base))
.await
.expect("create_dir_all failed");
transport
.write_file(
std::path::Path::new(&remote_target),
b"target content",
SystemTime::now(),
)
.await
.expect("write_file failed");
transport
.create_symlink(
std::path::Path::new("target.txt"),
std::path::Path::new(&remote_link),
)
.await
.expect("create_symlink failed");
let entries = transport
.scan(std::path::Path::new(&remote_base))
.await
.expect("scan failed");
let link_entry = entries
.iter()
.find(|e| e.path.ends_with("link.txt"))
.expect("Link should exist");
assert!(link_entry.is_symlink, "Should be a symlink");
cleanup_remote_path(&remote_base);
println!("✅ SSH create_symlink: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_read_nonexistent_file() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("nonexistent");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let result = transport
.read_file(std::path::Path::new(&remote_path))
.await;
assert!(result.is_err(), "Reading nonexistent file should fail");
println!("✅ SSH read_nonexistent_file: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_remove_nonexistent_file() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("nonexistent_remove");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let result = transport
.remove(std::path::Path::new(&remote_path), false)
.await;
println!("Remove nonexistent result: {:?}", result);
println!("✅ SSH remove_nonexistent_file: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_write_to_readonly_parent() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_base = create_remote_test_path("readonly");
let remote_file = format!("{}/file.txt", remote_base);
let setup_cmd = format!(
"ssh {} 'mkdir -p {} && chmod 444 {}'",
FEDORA_HOST, remote_base, remote_base
);
std::process::Command::new("sh")
.arg("-c")
.arg(&setup_cmd)
.output()
.expect("Failed to setup readonly directory");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let result = transport
.write_file(
std::path::Path::new(&remote_file),
b"test",
SystemTime::now(),
)
.await;
assert!(result.is_err(), "Writing to readonly directory should fail");
let cleanup_cmd = format!(
"ssh {} 'chmod 755 {} && rm -rf {}'",
FEDORA_HOST, remote_base, remote_base
);
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(&cleanup_cmd)
.output();
println!("✅ SSH write_to_readonly_parent: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_special_characters_in_filename() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_base = create_remote_test_path("special_chars");
let remote_file = format!("{}/file with spaces & special chars!.txt", remote_base);
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
transport
.create_dir_all(std::path::Path::new(&remote_base))
.await
.expect("create_dir_all failed");
let result = transport
.write_file(
std::path::Path::new(&remote_file),
b"special content",
SystemTime::now(),
)
.await;
if result.is_ok() {
let read_data = transport
.read_file(std::path::Path::new(&remote_file))
.await
.expect("read_file failed");
assert_eq!(read_data, b"special content");
println!("✅ SSH special_characters_in_filename: PASS");
} else {
println!("⚠️ SSH special_characters_in_filename: SKIP (not supported)");
}
cleanup_remote_path(&remote_base);
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_deep_directory_hierarchy() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_base = create_remote_test_path("deep");
let mut deep_path = remote_base.clone();
for i in 0..50 {
deep_path.push_str(&format!("/level{}", i));
}
deep_path.push_str("/file.txt");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let parent = std::path::Path::new(&deep_path).parent().unwrap();
transport
.create_dir_all(parent)
.await
.expect("create_dir_all failed for deep hierarchy");
transport
.write_file(
std::path::Path::new(&deep_path),
b"deep file",
SystemTime::now(),
)
.await
.expect("write_file failed at deep level");
let read_data = transport
.read_file(std::path::Path::new(&deep_path))
.await
.expect("read_file failed at deep level");
assert_eq!(read_data, b"deep file");
cleanup_remote_path(&remote_base);
println!("✅ SSH deep_directory_hierarchy: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_binary_data_integrity() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_path = create_remote_test_path("binary");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let mut binary_data = Vec::new();
for i in 0..=255u8 {
binary_data.extend_from_slice(&[i; 256]); }
transport
.write_file(
std::path::Path::new(&remote_path),
&binary_data,
SystemTime::now(),
)
.await
.expect("write_file failed");
let read_data = transport
.read_file(std::path::Path::new(&remote_path))
.await
.expect("read_file failed");
assert_eq!(
read_data.len(),
binary_data.len(),
"Binary data length mismatch"
);
assert_eq!(read_data, binary_data, "Binary data integrity check failed");
cleanup_remote_path(&remote_path);
println!("✅ SSH binary_data_integrity: PASS");
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_large_file_100mb() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_source = create_remote_test_path("large_100mb");
let create_cmd = format!(
"ssh {} 'dd if=/dev/zero of={} bs=1M count=100 2>/dev/null'",
FEDORA_HOST, remote_source
);
println!("Creating 100MB test file on fedora...");
std::process::Command::new("sh")
.arg("-c")
.arg(&create_cmd)
.output()
.expect("Failed to create 100MB file");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
let local_dest = TempDir::new().unwrap().path().join("large_100mb.dat");
println!("Downloading 100MB file...");
let start = std::time::Instant::now();
let result = transport
.copy_file_streaming(std::path::Path::new(&remote_source), &local_dest, None)
.await
.expect("Download failed");
let duration = start.elapsed();
let speed_mbps = (100.0 / duration.as_secs_f64()).round();
assert_eq!(result.bytes_written, 100 * 1024 * 1024);
assert!(local_dest.exists());
cleanup_remote_path(&remote_source);
println!(
"✅ SSH large_file_100mb: PASS ({:.2}s @ {} MB/s)",
duration.as_secs_f64(),
speed_mbps
);
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_many_small_files() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_base = create_remote_test_path("many_files");
let config = create_fedora_config();
let transport = SshTransport::new(&config).await.expect("Failed to connect");
transport
.create_dir_all(std::path::Path::new(&remote_base))
.await
.expect("create_dir_all failed");
println!("Creating 100 small files...");
let start = std::time::Instant::now();
for i in 0..100 {
let file_path = format!("{}/file_{:03}.txt", remote_base, i);
let content = format!("File number {}", i);
transport
.write_file(
std::path::Path::new(&file_path),
content.as_bytes(),
SystemTime::now(),
)
.await
.expect("write_file failed");
}
let duration = start.elapsed();
let entries = transport
.scan(std::path::Path::new(&remote_base))
.await
.expect("scan failed");
assert!(
entries.len() >= 100,
"Expected at least 100 files, got {}",
entries.len()
);
cleanup_remote_path(&remote_base);
println!(
"✅ SSH many_small_files: PASS (100 files in {:.2}s)",
duration.as_secs_f64()
);
}
#[tokio::test]
#[serial_test::serial]
#[ignore]
async fn test_ssh_connection_pool_concurrent_transfers() {
use msy::transport::ssh::SshTransport;
use msy::transport::Transport;
let remote_base = create_remote_test_path("pool_test");
for i in 1..=3 {
let file_path = format!("{}/file{}.dat", remote_base, i);
let create_cmd = format!(
"ssh {} 'mkdir -p {} && dd if=/dev/zero of={} bs=1M count=5 2>/dev/null'",
FEDORA_HOST, remote_base, file_path
);
std::process::Command::new("sh")
.arg("-c")
.arg(&create_cmd)
.output()
.expect("Failed to create remote file");
}
let config = create_fedora_config();
let transport = Arc::new(
SshTransport::with_pool_size(&config, 3)
.await
.expect("Failed to create pool"),
);
let dest_dir = TempDir::new().unwrap();
let mut handles = vec![];
for i in 1..=3 {
let t = transport.clone();
let remote_file = format!("{}/file{}.dat", remote_base, i);
let local_dest = dest_dir.path().join(format!("file{}.dat", i));
let handle = tokio::spawn(async move {
t.copy_file_streaming(std::path::Path::new(&remote_file), &local_dest, None)
.await
.unwrap()
});
handles.push(handle);
}
for handle in handles {
let result = handle.await.unwrap();
assert_eq!(result.bytes_written, 5 * 1024 * 1024);
}
cleanup_remote_path(&remote_base);
println!("✅ SSH connection_pool_concurrent_transfers: PASS");
}
#[test]
#[ignore]
fn print_ssh_test_summary() {
println!("\n========================================");
println!("SSH Comprehensive Test Suite Summary");
println!("========================================\n");
println!("SECTION 1: Basic Transport Operations");
println!(" - scan_directory");
println!(" - exists");
println!(" - create_dir_all");
println!(" - read_write_file");
println!(" - remove");
println!(" - get_mtime");
println!(" - file_info");
println!("\nSECTION 2: File Transfer Operations");
println!(" - copy_file_basic");
println!(" - copy_file_with_progress");
println!(" - copy_empty_file");
println!("\nSECTION 3: Symlink Operations");
println!(" - create_symlink");
println!("\nSECTION 4: Error Scenarios");
println!(" - read_nonexistent_file");
println!(" - remove_nonexistent_file");
println!(" - write_to_readonly_parent");
println!("\nSECTION 5: Edge Cases");
println!(" - special_characters_in_filename");
println!(" - deep_directory_hierarchy");
println!(" - binary_data_integrity");
println!("\nSECTION 6: Large Scale Operations");
println!(" - large_file_100mb");
println!(" - many_small_files");
println!("\nSECTION 7: Connection Pool");
println!(" - connection_pool_concurrent_transfers");
println!("\nTotal: 23 comprehensive SSH tests");
println!("\nRun with:");
println!(" cargo test --test ssh_comprehensive_test -- --ignored");
println!("========================================\n");
}