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

368 lines
12 KiB
Rust

//! Unit tests for handle_online_organization handler
//!
//! Tests the online (AI-powered) file organization functionality including:
//! - Args and Config creation
//! - FileBatch handling
//! - Text file detection for deep inspection
//! - File sample reading
//! - API error handling (graceful degradation)
use noentropy::cli::Args;
use noentropy::cli::handlers::handle_online_organization;
use noentropy::files::{FileBatch, is_text_file, read_file_sample};
use noentropy::settings::Config;
use noentropy::storage::{Cache, UndoLog};
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/// Helper to create test Args with default values
fn create_test_args(dry_run: bool, max_concurrent: usize) -> Args {
Args {
dry_run,
max_concurrent,
recursive: false,
undo: false,
change_key: false,
offline: false,
duplicate: false,
path: None,
}
}
/// Helper to create a test Config
fn create_test_config(api_key: &str) -> Config {
Config {
api_key: api_key.to_string(),
download_folder: PathBuf::new(),
categories: vec![
"Images".to_string(),
"Documents".to_string(),
"Code".to_string(),
"Music".to_string(),
"Video".to_string(),
"Archives".to_string(),
],
}
}
/// Helper to create a FileBatch from filenames
fn create_file_batch(filenames: Vec<String>, base_path: &Path) -> FileBatch {
let paths: Vec<PathBuf> = filenames.iter().map(|f| base_path.join(f)).collect();
FileBatch { filenames, paths }
}
/// Helper to setup a temp directory with test files
fn setup_test_dir_with_files(files: &[(&str, Option<&str>)]) -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().to_path_buf();
for (filename, content) in files {
let file_path = dir_path.join(filename);
let mut file = File::create(&file_path).unwrap();
if let Some(text) = content {
file.write_all(text.as_bytes()).unwrap();
}
}
(temp_dir, dir_path)
}
// ============================================================================
// ARGS TESTS
// ============================================================================
#[test]
fn test_args_creation() {
let args = create_test_args(true, 10);
assert!(args.dry_run);
assert_eq!(args.max_concurrent, 10);
assert!(!args.recursive);
assert!(!args.offline);
}
#[test]
fn test_args_default_max_concurrent() {
let args = create_test_args(false, 5);
assert_eq!(args.max_concurrent, 5);
}
#[test]
fn test_args_all_flags() {
let args = Args {
dry_run: true,
max_concurrent: 10,
recursive: true,
undo: true,
change_key: true,
offline: true,
path: Some(PathBuf::from("/test/path")),
duplicate: true,
};
assert!(args.dry_run);
assert!(args.recursive);
assert!(args.undo);
assert!(args.change_key);
assert!(args.offline);
assert_eq!(args.path, Some(PathBuf::from("/test/path")));
}
// ============================================================================
// CONFIG TESTS
// ============================================================================
#[test]
fn test_config_creation() {
let config = create_test_config("test-api-key");
assert_eq!(config.api_key, "test-api-key");
assert_eq!(config.categories.len(), 6);
assert!(config.categories.contains(&"Images".to_string()));
}
#[test]
fn test_config_with_custom_categories() {
let config = Config {
api_key: "key".to_string(),
download_folder: PathBuf::from("/test"),
categories: vec!["Custom1".to_string(), "Custom2".to_string()],
};
assert_eq!(config.categories.len(), 2);
assert!(config.categories.contains(&"Custom1".to_string()));
}
#[test]
fn test_config_empty_categories() {
let config = Config {
api_key: "key".to_string(),
download_folder: PathBuf::new(),
categories: vec![],
};
assert!(config.categories.is_empty());
}
// ============================================================================
// FILE BATCH TESTS
// ============================================================================
#[test]
fn test_file_batch_creation() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path();
let filenames = vec!["test.txt".to_string(), "image.jpg".to_string()];
let batch = create_file_batch(filenames.clone(), dir_path);
assert_eq!(batch.filenames.len(), 2);
assert_eq!(batch.paths.len(), 2);
assert!(batch.paths[0].ends_with("test.txt"));
assert!(batch.paths[1].ends_with("image.jpg"));
}
#[test]
fn test_file_batch_empty() {
let temp_dir = TempDir::new().unwrap();
let batch = create_file_batch(vec![], temp_dir.path());
assert!(batch.filenames.is_empty());
assert!(batch.paths.is_empty());
}
#[test]
fn test_file_batch_count() {
let temp_dir = TempDir::new().unwrap();
let filenames: Vec<String> = (0..10).map(|i| format!("file{}.txt", i)).collect();
let batch = create_file_batch(filenames, temp_dir.path());
assert_eq!(batch.count(), 10);
}
// ============================================================================
// TEXT FILE DETECTION TESTS (for deep inspection)
// ============================================================================
#[test]
fn test_text_file_detection_for_deep_inspection() {
let (_temp_dir, dir_path) = setup_test_dir_with_files(&[
("test.txt", Some("text content")),
("test.rs", Some("fn main() {}")),
("test.jpg", None),
]);
// Text files should be detected for deep inspection
assert!(is_text_file(&dir_path.join("test.txt")));
assert!(is_text_file(&dir_path.join("test.rs")));
}
#[test]
fn test_text_file_detection_various_extensions() {
let (_temp_dir, dir_path) = setup_test_dir_with_files(&[
("code.py", Some("print('hello')")),
("code.js", Some("console.log('hi')")),
("config.json", Some("{}")),
("config.yaml", Some("key: value")),
("doc.md", Some("# Title")),
]);
assert!(is_text_file(&dir_path.join("code.py")));
assert!(is_text_file(&dir_path.join("code.js")));
assert!(is_text_file(&dir_path.join("config.json")));
assert!(is_text_file(&dir_path.join("config.yaml")));
assert!(is_text_file(&dir_path.join("doc.md")));
}
// ============================================================================
// FILE SAMPLE READING TESTS
// ============================================================================
#[test]
fn test_read_file_sample_for_deep_inspection() {
let (_temp_dir, dir_path) =
setup_test_dir_with_files(&[("test.txt", Some("This is a test file with some content."))]);
let sample = read_file_sample(&dir_path.join("test.txt"), 100);
assert!(sample.is_some());
assert!(sample.unwrap().contains("test file"));
}
#[test]
fn test_read_file_sample_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let sample = read_file_sample(&temp_dir.path().join("nonexistent.txt"), 100);
assert!(sample.is_none());
}
#[test]
fn test_read_file_sample_truncation() {
let long_content = "x".repeat(1000);
let (_temp_dir, dir_path) = setup_test_dir_with_files(&[("long.txt", Some(&long_content))]);
let sample = read_file_sample(&dir_path.join("long.txt"), 100);
assert!(sample.is_some());
let sample_content = sample.unwrap();
assert!(sample_content.len() <= 100);
}
#[test]
fn test_read_file_sample_empty_file() {
let (_temp_dir, dir_path) = setup_test_dir_with_files(&[("empty.txt", Some(""))]);
let sample = read_file_sample(&dir_path.join("empty.txt"), 100);
assert!(sample.is_some());
assert!(sample.unwrap().is_empty());
}
#[test]
fn test_read_file_sample_exact_limit() {
let content = "x".repeat(100);
let (_temp_dir, dir_path) = setup_test_dir_with_files(&[("exact.txt", Some(&content))]);
let sample = read_file_sample(&dir_path.join("exact.txt"), 100);
assert!(sample.is_some());
assert_eq!(sample.unwrap().len(), 100);
}
// ============================================================================
// HANDLER ASYNC TESTS
// ============================================================================
#[tokio::test]
async fn test_handle_online_organization_requires_valid_api_key() {
// This test validates that the function correctly handles API setup
// In a real scenario, an invalid API key would result in an API error
let (_temp_dir, dir_path) = setup_test_dir_with_files(&[("test.txt", Some("content"))]);
let args = create_test_args(true, 5);
let config = create_test_config("invalid-api-key");
let batch = create_file_batch(vec!["test.txt".to_string()], &dir_path);
let mut cache = Cache::new();
let mut undo_log = UndoLog::new();
// The function should attempt to call the API
// With an invalid key, it will fail but should handle the error gracefully
let result =
handle_online_organization(&args, &config, batch, &dir_path, &mut cache, &mut undo_log)
.await;
// The function returns Ok(None) even on API errors (handled internally)
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_online_organization_empty_batch() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path();
let args = create_test_args(true, 5);
let config = create_test_config("test-key");
let batch = create_file_batch(vec![], dir_path);
let mut cache = Cache::new();
let mut undo_log = UndoLog::new();
// Empty batch should be handled gracefully
let result =
handle_online_organization(&args, &config, batch, dir_path, &mut cache, &mut undo_log)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_online_organization_dry_run() {
let (_temp_dir, dir_path) =
setup_test_dir_with_files(&[("photo.jpg", Some("image")), ("document.pdf", Some("pdf"))]);
let args = create_test_args(true, 5); // dry_run = true
let config = create_test_config("test-key");
let batch = create_file_batch(
vec!["photo.jpg".to_string(), "document.pdf".to_string()],
&dir_path,
);
let mut cache = Cache::new();
let mut undo_log = UndoLog::new();
let result =
handle_online_organization(&args, &config, batch, &dir_path, &mut cache, &mut undo_log)
.await;
assert!(result.is_ok());
// Files should still exist (dry run + API failure = no moves)
assert!(dir_path.join("photo.jpg").exists());
assert!(dir_path.join("document.pdf").exists());
}
// ============================================================================
// CACHE AND UNDO LOG TESTS
// ============================================================================
#[test]
fn test_cache_new() {
let cache = Cache::new();
// Just verify it can be created
assert!(true);
let _ = cache; // Use the variable to avoid warning
}
#[test]
fn test_undo_log_new() {
let undo_log = UndoLog::new();
assert_eq!(undo_log.get_completed_count(), 0);
assert!(!undo_log.has_completed_moves());
}
#[test]
fn test_undo_log_record_move() {
let mut undo_log = UndoLog::new();
undo_log.record_move(
PathBuf::from("/source/file.txt"),
PathBuf::from("/dest/file.txt"),
);
assert_eq!(undo_log.get_completed_count(), 1);
assert!(undo_log.has_completed_moves());
}