- Fix compilation error in path_utils.rs where path.display() was used after the path was moved into spawn_blocking closure - Add 12 tests for handle_undo handler covering no undo log, no completed moves, custom paths, dry run, invalid paths, etc. - Add 16 tests for validate_and_normalize_path covering path validation, directories, normalization, edge cases - Add 21 tests for cache module covering key generation, hit/miss behavior, eviction, persistence, etc. All 86 tests pass successfully.
322 lines
10 KiB
Rust
322 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,
|
|
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());
|
|
}
|