Files
noentropy/tests/test_undo_handler.rs
2026-01-13 19:13:26 +05:30

323 lines
10 KiB
Rust

//! Unit tests for handle_undo handler
//!
//! Tests the undo functionality including:
//! - No undo log exists
//! - No completed moves to undo
//! - Path validation
//! - Dry run behavior
//! - Successful undo operations
use noentropy::cli::Args;
use noentropy::cli::handlers::handle_undo;
use noentropy::cli::path_utils::validate_and_normalize_path;
use noentropy::storage::UndoLog;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/// Helper to create test Args
fn create_test_args(dry_run: bool, path: Option<PathBuf>) -> Args {
Args {
dry_run,
max_concurrent: 5,
recursive: false,
undo: true,
change_key: false,
offline: false,
duplicate: false,
path,
}
}
/// Helper to setup a temp directory with files and subdirectories for undo testing
fn setup_test_dir_for_undo() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
// Create source directory with files
let images_dir = dir_path.join("Images");
fs::create_dir_all(&images_dir).unwrap();
// Create files that were "moved" to the Images directory
let photo1 = images_dir.join("photo1.jpg");
let photo2 = images_dir.join("photo2.png");
File::create(&photo1).unwrap();
File::create(&photo2).unwrap();
// Create source locations that no longer exist (after move)
let source1 = dir_path.join("photo1.jpg");
let source2 = dir_path.join("photo2.png");
(temp_dir, dir_path)
}
/// Helper to create an undo log with completed moves
fn create_undo_log_with_moves(undo_log_path: &Path, moves: Vec<(PathBuf, PathBuf)>) {
let mut undo_log = UndoLog::new();
for (source, dest) in moves {
undo_log.record_move(source, dest);
}
undo_log.save(undo_log_path).unwrap();
}
// ============================================================================
// HANDLE_UNDO TESTS
// ============================================================================
#[tokio::test]
async fn test_handle_undo_no_undo_log_exists() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
let args = create_test_args(false, None);
// Don't create an undo log file - it should handle gracefully
let result = handle_undo(args, dir_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_undo_no_completed_moves() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
// Create an empty undo log (no completed moves)
let undo_log_path = dir_path.join("undo_log.json");
let undo_log = UndoLog::new();
undo_log.save(&undo_log_path).unwrap();
let args = create_test_args(false, None);
let result = handle_undo(args, dir_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_undo_with_custom_path() {
let temp_dir = TempDir::new().unwrap();
let custom_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
let custom_path = custom_dir.path().to_path_buf();
// Create undo log in the main temp dir
let undo_log_path = dir_path.join("undo_log.json");
// Create source files in custom path
let source_file = custom_path.join("test.txt");
File::create(&source_file).unwrap();
// Create destination (Images directory)
let images_dir = custom_path.join("Images");
fs::create_dir_all(&images_dir).unwrap();
let dest_file = images_dir.join("test.txt");
File::create(&dest_file).unwrap();
// Create undo log with a move
create_undo_log_with_moves(&undo_log_path, vec![(source_file.clone(), dest_file)]);
// Use custom path argument
let args = create_test_args(true, Some(custom_path.clone()));
let result = handle_undo(args, custom_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_undo_dry_run_no_changes() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
// Setup directory with files
let images_dir = dir_path.join("Images");
fs::create_dir_all(&images_dir).unwrap();
let photo = images_dir.join("photo.jpg");
let source = dir_path.join("photo.jpg");
File::create(&photo).unwrap();
// Create undo log
let undo_log_path = dir_path.join("undo_log.json");
create_undo_log_with_moves(&undo_log_path, vec![(source.clone(), photo.clone())]);
// Dry run should not actually undo
let args = create_test_args(true, None);
let result = handle_undo(args, dir_path).await;
assert!(result.is_ok());
// File should still be in Images directory (dry run)
assert!(photo.exists());
// Source should still NOT exist (dry run)
assert!(!source.exists());
}
#[tokio::test]
async fn test_handle_undo_invalid_path() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
// Create undo log with some completed moves
let undo_log_path = dir_path.join("undo_log.json");
let mut undo_log = UndoLog::new();
undo_log.record_move(
PathBuf::from("/source/file.txt"),
PathBuf::from("/dest/file.txt"),
);
undo_log.save(&undo_log_path).unwrap();
// Use a non-existent path
let invalid_path = dir_path.join("nonexistent_directory");
let args = create_test_args(false, Some(invalid_path.clone()));
let result = handle_undo(args, invalid_path).await;
// Should handle error gracefully and return Ok
assert!(result.is_ok());
}
// ============================================================================
// VALIDATE_AND_NORMALIZE_PATH TESTS
// ============================================================================
#[tokio::test]
async fn test_validate_and_normalize_path_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let nonexistent = temp_dir.path().join("nonexistent");
let result = validate_and_normalize_path(&nonexistent).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("does not exist"));
}
#[tokio::test]
async fn test_validate_and_normalize_path_is_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
File::create(&file_path).unwrap();
let result = validate_and_normalize_path(&file_path).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("not a directory"));
}
#[tokio::test]
async fn test_validate_and_normalize_path_success() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path();
let result = validate_and_normalize_path(dir_path).await;
assert!(result.is_ok());
let normalized = result.unwrap();
// The canonicalized path should be equivalent
assert_eq!(normalized, dir_path.canonicalize().unwrap());
}
#[tokio::test]
async fn test_validate_and_normalize_path_empty_dir() {
let temp_dir = TempDir::new().unwrap();
let empty_dir = temp_dir.path();
let result = validate_and_normalize_path(empty_dir).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_and_normalize_path_with_subdirs() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("subdir").join("nested");
fs::create_dir_all(&subdir).unwrap();
let result = validate_and_normalize_path(&subdir).await;
assert!(result.is_ok());
let normalized = result.unwrap();
assert!(normalized.to_string_lossy().contains("nested"));
}
// ============================================================================
// EDGE CASE TESTS
// ============================================================================
#[tokio::test]
async fn test_handle_undo_multiple_moves_dry_run() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
// Setup multiple files
let images_dir = dir_path.join("Images");
let docs_dir = dir_path.join("Documents");
fs::create_dir_all(&images_dir).unwrap();
fs::create_dir_all(&docs_dir).unwrap();
let files = [
(dir_path.join("photo1.jpg"), images_dir.join("photo1.jpg")),
(dir_path.join("photo2.jpg"), images_dir.join("photo2.jpg")),
(dir_path.join("doc1.pdf"), docs_dir.join("doc1.pdf")),
];
for (source, dest) in &files {
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).unwrap();
}
File::create(dest).unwrap();
}
// Create undo log
let undo_log_path = dir_path.join("undo_log.json");
let moves: Vec<(PathBuf, PathBuf)> = files.iter().cloned().collect();
create_undo_log_with_moves(&undo_log_path, moves);
// Dry run
let args = create_test_args(true, None);
let result = handle_undo(args, dir_path).await;
assert!(result.is_ok());
// All destination files should still exist
assert!(images_dir.join("photo1.jpg").exists());
assert!(images_dir.join("photo2.jpg").exists());
assert!(docs_dir.join("doc1.pdf").exists());
}
#[tokio::test]
async fn test_handle_undo_logs_saved() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
// Setup directory with file
let images_dir = dir_path.join("Images");
fs::create_dir_all(&images_dir).unwrap();
let photo = images_dir.join("photo.jpg");
let source = dir_path.join("photo.jpg");
File::create(&photo).unwrap();
// Create undo log
let undo_log_path = dir_path.join("undo_log.json");
create_undo_log_with_moves(&undo_log_path, vec![(source, photo.clone())]);
// Create a new temp dir for target (to avoid cleanup issues)
let target_temp = TempDir::new().unwrap();
let target_path = target_temp.path().to_path_buf();
// Copy the undo log to the new location
fs::copy(&undo_log_path, target_path.join("undo_log.json")).unwrap();
// Copy the file structure
fs::create_dir_all(&target_path.join("Images")).unwrap();
fs::copy(&photo, target_path.join("Images").join("photo.jpg")).unwrap();
// Run undo with --dry-run to test it doesn't fail on save
let args = create_test_args(true, None);
let result = handle_undo(args, target_path).await;
assert!(result.is_ok());
}