/ app / tests / main_test.rs
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  }