main_test.rs
1 use anyhow::{Result, bail}; 2 use std::path::{Path, PathBuf}; 3 use std::ffi::OsStr; 4 5 // Mock SSH session and remote command functions for testing 6 use std::collections::HashMap; 7 8 struct MockSession; 9 10 impl MockSession { 11 fn new() -> Self { MockSession } 12 } 13 14 struct MockState { 15 remote_fs: HashMap<PathBuf, RemoteFsEntry>, 16 remote_cmd_called_with: Option<String>, 17 remote_cmd_fail: bool, 18 } 19 20 enum RemoteFsEntry { 21 Directory, 22 File, 23 } 24 25 impl MockState { 26 fn new() -> Self { 27 MockState { 28 remote_fs: HashMap::new(), 29 remote_cmd_called_with: None, 30 remote_cmd_fail: false, 31 } 32 } 33 34 fn add_dir(&mut self, path: &str) { 35 let mut current_path = PathBuf::new(); 36 for component in PathBuf::from(path).components() { 37 current_path.push(component); 38 self.remote_fs.insert(current_path.clone(), RemoteFsEntry::Directory); 39 } 40 } 41 42 fn add_file(&mut self, path: &str) { 43 let file_path = PathBuf::from(path); 44 if let Some(parent) = file_path.parent() { 45 self.add_dir(parent.to_str().unwrap()); // Ensure parent directories exist 46 } 47 self.remote_fs.insert(file_path, RemoteFsEntry::File); 48 } 49 } 50 51 // Mock implementation of run_remote_test 52 fn run_remote_test(_sess: &MockSession, cmd: &str, state: &mut MockState) -> Result<bool> { 53 let parts: Vec<&str> = cmd.split_whitespace().collect(); 54 if parts.len() < 3 || (parts[0] != "test" || (parts[1] != "-e" && parts[1] != "-d")) { 55 bail!("Invalid mock test command: {}", cmd); 56 } 57 let path_str = parts[2].trim_matches('\''); 58 let path = PathBuf::from(path_str); 59 60 match state.remote_fs.get(&path) { 61 Some(entry) => { 62 if parts[1] == "-e" { // test -e (exists) 63 Ok(true) 64 } else { // test -d (is directory) 65 match entry { 66 RemoteFsEntry::Directory => Ok(true), 67 RemoteFsEntry::File => Ok(false), 68 } 69 } 70 } 71 None => Ok(false), // Path does not exist 72 } 73 } 74 75 // Mock implementation of run_remote_cmd 76 fn run_remote_cmd(_sess: &MockSession, cmd: &str, state: &mut MockState) -> Result<()> { 77 state.remote_cmd_called_with = Some(cmd.to_string()); 78 if state.remote_cmd_fail { 79 bail!("Mock remote command failed"); 80 } 81 // Simulate mkdir -p by adding the directory and its parents to the mock file system 82 if cmd.starts_with("mkdir -p ") { 83 let path_str = cmd.trim_start_matches("mkdir -p ").trim_matches('\''); 84 state.add_dir(path_str); 85 } 86 Ok(()) 87 } 88 89 // Re-declare the function from main.rs for testing purposes 90 // In a real scenario, you might make this function public or use a test-specific module structure. 91 fn determine_and_validate_remote_path( 92 sess: &MockSession, 93 path: &Path, 94 source_basename: &OsStr, 95 _is_source_dir: bool, 96 state: &mut MockState, // Add state parameter 97 ) -> Result<PathBuf> { 98 let path_str = path.to_string_lossy(); 99 100 // Case 1: Remote path exists 101 if run_remote_test(sess, &format!("test -e '{}'", path_str), state)? { 102 if run_remote_test(sess, &format!("test -d '{}'", path_str), state)? { 103 // Remote path exists and is a directory. 104 // We need to check if the final target (source_basename inside path) already exists. 105 let final_target_path_on_remote = path.join(source_basename); 106 if run_remote_test(sess, &format!("test -e '{}'", final_target_path_on_remote.to_string_lossy()), state)? { 107 bail!( 108 "Final destination '{}' already exists on remote.", 109 final_target_path_on_remote.display() 110 ); 111 } 112 return Ok(path.to_path_buf()); 113 } else { 114 // Remote path exists but is not a directory (it's a file). 115 bail!("Remote path '{}' exists but is not a directory.", path_str); 116 } 117 } 118 119 // Case 2: Remote path does not exist. We need to create it or its parent must exist. 120 if let Some(parent) = path.parent() { 121 let parent_str = parent.to_string_lossy(); 122 if parent_str.is_empty() { 123 if !run_remote_test(sess, "test -d /", state)? { 124 bail!( 125 "Remote root directory '/' does not exist or is not a directory. This is highly unusual." 126 ); 127 } 128 } else if !run_remote_test(sess, &format!("test -d '{}'", parent_str), state)? { 129 bail!( 130 "Remote parent path '{}' does not exist or is not a directory.", 131 parent.display() 132 ); 133 } 134 135 // Parent exists and is a directory. We will create `path`. 136 run_remote_cmd(sess, &format!("mkdir -p '{}'", path_str), state)?; 137 Ok(path.to_path_buf()) 138 } else { 139 bail!( 140 "Could not determine parent of remote path '{}'.", 141 path.display() 142 ); 143 } 144 } 145 146 #[cfg(test)] 147 mod tests { 148 use super::*; 149 use bale::progress::TransferProgress; 150 151 152 #[test] 153 fn test_determine_remote_path_dir_dest_exists_dir_empty() -> Result<()> { 154 let mut state = MockState::new(); 155 state.add_dir("/existing/dir"); 156 let sess = MockSession::new(); 157 let remote_path = PathBuf::from("/existing/dir"); 158 let source_basename = OsStr::new("source_dir"); 159 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, true, &mut state)?; 160 assert_eq!(result, PathBuf::from("/existing/dir")); 161 Ok(()) 162 } 163 164 #[test] 165 fn test_determine_remote_path_dir_dest_exists_dir_target_exists() { 166 let mut state = MockState::new(); 167 state.add_dir("/existing/dir"); 168 state.add_dir("/existing/dir/target_dir"); 169 let sess = MockSession::new(); 170 let remote_path = PathBuf::from("/existing/dir"); 171 let source_basename = OsStr::new("target_dir"); 172 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, true, &mut state); 173 assert!(result.is_err()); 174 assert_eq!( 175 result.unwrap_err().to_string(), 176 "Final destination '/existing/dir/target_dir' already exists on remote." 177 ); 178 } 179 180 #[test] 181 fn test_determine_remote_path_dir_dest_exists_file() { 182 let mut state = MockState::new(); 183 state.add_file("/file_not_dir"); 184 let sess = MockSession::new(); 185 let remote_path = PathBuf::from("/file_not_dir"); 186 let source_basename = OsStr::new("source_dir"); 187 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, true, &mut state); 188 assert!(result.is_err()); 189 assert_eq!( 190 result.unwrap_err().to_string(), 191 "Remote path '/file_not_dir' exists but is not a directory." 192 ); 193 } 194 195 #[test] 196 fn test_determine_remote_path_dir_dest_does_not_exist_parent_exists() -> Result<()> { 197 let mut state = MockState::new(); 198 state.add_dir("/parent/of/new"); 199 let sess = MockSession::new(); 200 let remote_path = PathBuf::from("/parent/of/new/new_dir"); 201 let source_basename = OsStr::new("source_dir"); 202 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, true, &mut state)?; 203 assert_eq!(result, PathBuf::from("/parent/of/new/new_dir")); 204 assert_eq!(state.remote_cmd_called_with, Some("mkdir -p '/parent/of/new/new_dir'".to_string())); 205 Ok(()) 206 } 207 208 #[test] 209 fn test_determine_remote_path_dir_dest_does_not_exist_parent_does_not_exist() { 210 let mut state = MockState::new(); 211 let sess = MockSession::new(); 212 let remote_path = PathBuf::from("/nonexistent/parent/new_dir"); 213 let source_basename = OsStr::new("source_dir"); 214 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, true, &mut state); 215 assert!(result.is_err()); 216 assert_eq!( 217 result.unwrap_err().to_string(), 218 "Remote parent path '/nonexistent/parent' does not exist or is not a directory." 219 ); 220 } 221 222 #[test] 223 fn test_determine_remote_path_file_dest_exists_dir_empty() -> Result<()> { 224 let mut state = MockState::new(); 225 state.add_dir("/existing/dir"); 226 // Do NOT add /existing/dir/source_file.txt to state, so it doesn't exist 227 let sess = MockSession::new(); 228 let remote_path = PathBuf::from("/existing/dir"); 229 let source_basename = OsStr::new("source_file.txt"); 230 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, false, &mut state)?; 231 assert_eq!(result, PathBuf::from("/existing/dir")); 232 Ok(()) 233 } 234 235 #[test] 236 fn test_determine_remote_path_file_dest_exists_dir_target_exists() { 237 let mut state = MockState::new(); 238 state.add_dir("/existing/dir"); 239 state.add_file("/existing/dir/target_file.txt"); // Simulate the target file existing 240 let sess = MockSession::new(); 241 let remote_path = PathBuf::from("/existing/dir"); 242 let source_basename = OsStr::new("target_file.txt"); 243 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, false, &mut state); 244 assert!(result.is_err()); 245 assert_eq!( 246 result.unwrap_err().to_string(), 247 "Final destination '/existing/dir/target_file.txt' already exists on remote." 248 ); 249 } 250 251 #[test] 252 fn test_determine_remote_path_file_dest_exists_file() { 253 let mut state = MockState::new(); 254 state.add_file("/file_not_dir"); 255 let sess = MockSession::new(); 256 let remote_path = PathBuf::from("/file_not_dir"); 257 let source_basename = OsStr::new("source_file.txt"); 258 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, false, &mut state); 259 assert!(result.is_err()); 260 assert_eq!( 261 result.unwrap_err().to_string(), 262 "Remote path '/file_not_dir' exists but is not a directory." 263 ); 264 } 265 266 #[test] 267 fn test_determine_remote_path_file_dest_does_not_exist_parent_exists() -> Result<()> { 268 let mut state = MockState::new(); 269 state.add_dir("/parent/of/new"); 270 let sess = MockSession::new(); 271 let remote_path = PathBuf::from("/parent/of/new/new_file"); 272 let source_basename = OsStr::new("source_file.txt"); 273 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, false, &mut state)?; 274 assert_eq!(result, PathBuf::from("/parent/of/new/new_file")); 275 assert_eq!(state.remote_cmd_called_with, Some("mkdir -p '/parent/of/new/new_file'".to_string())); 276 Ok(()) 277 } 278 279 #[test] 280 fn test_determine_remote_path_file_dest_does_not_exist_parent_does_not_exist() { 281 let mut state = MockState::new(); 282 let sess = MockSession::new(); 283 let remote_path = PathBuf::from("/nonexistent/parent/new_file"); 284 let source_basename = OsStr::new("source_file.txt"); 285 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, false, &mut state); 286 assert!(result.is_err()); 287 assert_eq!( 288 result.unwrap_err().to_string(), 289 "Remote parent path '/nonexistent/parent' does not exist or is not a directory." 290 ); 291 } 292 293 #[test] 294 fn test_determine_remote_path_root_dir_exists() -> Result<()> { 295 let mut state = MockState::new(); 296 state.add_dir("/"); 297 let sess = MockSession::new(); 298 let remote_path = PathBuf::from("/"); 299 let source_basename = OsStr::new("source_dir"); 300 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, true, &mut state)?; 301 assert_eq!(result, PathBuf::from("/")); 302 Ok(()) 303 } 304 305 #[test] 306 fn test_determine_remote_path_root_dir_file_exists() { 307 let mut state = MockState::new(); 308 state.add_dir("/"); 309 state.add_dir("/source_dir"); 310 let sess = MockSession::new(); 311 let remote_path = PathBuf::from("/"); 312 let source_basename = OsStr::new("source_dir"); 313 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, true, &mut state); 314 assert!(result.is_err()); 315 assert_eq!( 316 result.unwrap_err().to_string(), 317 "Final destination '/source_dir' already exists on remote." 318 ); 319 } 320 321 #[test] 322 fn test_determine_remote_path_mkdir_fails() { 323 let mut state = MockState::new(); 324 state.add_dir("/new"); 325 state.remote_cmd_fail = true; 326 let sess = MockSession::new(); 327 let remote_path = PathBuf::from("/new/path"); 328 let source_basename = OsStr::new("source_dir"); 329 let result = determine_and_validate_remote_path(&sess, &remote_path, source_basename, true, &mut state); 330 assert!(result.is_err()); 331 assert_eq!( 332 result.unwrap_err().to_string(), 333 "Mock remote command failed" 334 ); 335 } 336 337 #[test] 338 fn test_multiple_sources_validation() -> Result<()> { 339 // Test that the validation logic handles multiple sources correctly 340 // This is a basic test for the validation function 341 let sources = vec![ 342 PathBuf::from("/tmp/test1"), 343 PathBuf::from("/tmp/test2"), 344 ]; 345 346 // The actual validation happens in main.rs, but we can test the concept 347 let mut seen_names = std::collections::HashSet::new(); 348 for source in &sources { 349 if let Some(name) = source.file_name().and_then(|n| n.to_str()) { 350 assert!(seen_names.insert(name.to_string()), "Duplicate name found: {}", name); 351 } 352 } 353 354 Ok(()) 355 } 356 357 #[test] 358 fn test_multiple_sources_duplicate_names() { 359 let sources = vec![ 360 PathBuf::from("/tmp/test1"), 361 PathBuf::from("/other/test1"), // Same basename as above 362 ]; 363 364 let mut seen_names = std::collections::HashSet::new(); 365 let mut has_duplicate = false; 366 367 for source in &sources { 368 if let Some(name) = source.file_name().and_then(|n| n.to_str()) { 369 if !seen_names.insert(name.to_string()) { 370 has_duplicate = true; 371 break; 372 } 373 } 374 } 375 376 assert!(has_duplicate, "Should detect duplicate basenames"); 377 } 378 379 #[test] 380 fn test_progress_multiple_sources_calculation() -> Result<()> { 381 // Create temporary test files and directories 382 let temp_dir = tempfile::tempdir()?; 383 let file1 = temp_dir.path().join("test1.txt"); 384 let file2 = temp_dir.path().join("test2.txt"); 385 let dir1 = temp_dir.path().join("dir1"); 386 387 std::fs::write(&file1, "hello")?; 388 std::fs::write(&file2, "world")?; 389 std::fs::create_dir(&dir1)?; 390 std::fs::write(dir1.join("nested.txt"), "nested content")?; 391 392 let sources = vec![file1, file2, dir1]; 393 let progress = TransferProgress::new(); 394 395 let (total_files, total_bytes) = progress.calculate_multiple_sources_info(&sources)?; 396 397 // Should have 3 files: test1.txt, test2.txt, and nested.txt 398 assert_eq!(total_files, 3); 399 // Total bytes should be sum of all file contents 400 assert_eq!(total_bytes, 5 + 5 + 14); 401 402 Ok(()) 403 } 404 }