//! Comprehensive test suite for the parts todo system. //! //! Tests cover all core functionality including content manipulation, //! command operations, filtering, and edge cases. use crate::commands::*; use crate::content::*; /// Tests adding an entry to empty content #[test] fn test_add_entry_to_content_empty() { let result = add_entry_to_content("", "First item"); assert_eq!(result, "First item\n"); } /// Tests adding an entry to existing content #[test] fn test_add_entry_to_content_existing() { let existing = "Second item\nThird item"; let result = add_entry_to_content(existing, "First item"); assert_eq!(result, "First item\nSecond item\nThird item"); } /// Tests adding an entry when existing content has trailing newline #[test] fn test_add_entry_to_content_with_trailing_newline() { let existing = "Second item\nThird item\n"; let result = add_entry_to_content(existing, "First item"); assert_eq!(result, "First item\nSecond item\nThird item"); } /// Tests prioritizing a single item by moving it to the top #[test] fn test_prioritize_items_in_content_single_item() { let lines = vec![ "First item".to_string(), "Second item".to_string(), "Third item".to_string(), ]; let result = prioritize_items_in_content(&lines, &[2]); assert!(result.is_ok()); let (new_content, prioritized) = result.unwrap(); assert_eq!(new_content, "Second item\nFirst item\nThird item\n"); assert_eq!(prioritized, vec!["Second item"]); } /// Tests prioritizing multiple items in command-line order #[test] fn test_prioritize_items_in_content_multiple_items() { let lines = vec![ "First item".to_string(), "Second item".to_string(), "Third item".to_string(), "Fourth item".to_string(), ]; let result = prioritize_items_in_content(&lines, &[3, 1]); assert!(result.is_ok()); let (new_content, prioritized) = result.unwrap(); assert_eq!( new_content, "Third item\nFirst item\nSecond item\nFourth item\n" ); assert_eq!(prioritized, vec!["Third item", "First item"]); } /// Tests error handling for invalid item numbers #[test] fn test_prioritize_items_in_content_invalid_number() { let lines = vec!["First item".to_string(), "Second item".to_string()]; let result = prioritize_items_in_content(&lines, &[3]); assert!(result.is_err()); assert_eq!( result.unwrap_err(), "Invalid item number: 3. Valid range: 1-2" ); } /// Tests error handling for zero item numbers #[test] fn test_prioritize_items_in_content_zero_number() { let lines = vec!["First item".to_string()]; let result = prioritize_items_in_content(&lines, &[0]); assert!(result.is_err()); assert_eq!( result.unwrap_err(), "Invalid item number: 0. Valid range: 1-1" ); } /// Tests duplicate removal while preserving order #[test] fn test_prioritize_items_in_content_duplicates() { let lines = vec![ "First item".to_string(), "Second item".to_string(), "Third item".to_string(), ]; let result = prioritize_items_in_content(&lines, &[2, 2, 1]); assert!(result.is_ok()); let (new_content, prioritized) = result.unwrap(); assert_eq!(new_content, "Second item\nFirst item\nThird item\n"); assert_eq!(prioritized, vec!["Second item", "First item"]); } /// Tests archiving a single item #[test] fn test_archive_items_in_content_single_item() { let lines = vec![ "First item".to_string(), "Second item".to_string(), "Third item".to_string(), ]; let result = archive_items_in_content(&lines, &[2]); assert!(result.is_ok()); let (new_content, archived) = result.unwrap(); assert_eq!(new_content, "First item\nThird item\n"); assert_eq!(archived, vec!["Second item"]); } /// Tests archiving multiple items #[test] fn test_archive_items_in_content_multiple_items() { let lines = vec![ "First item".to_string(), "Second item".to_string(), "Third item".to_string(), "Fourth item".to_string(), ]; let result = archive_items_in_content(&lines, &[1, 3]); assert!(result.is_ok()); let (new_content, archived) = result.unwrap(); assert_eq!(new_content, "Second item\nFourth item\n"); assert_eq!(archived, vec!["First item", "Third item"]); } /// Tests archiving all items results in empty content #[test] fn test_archive_items_in_content_all_items() { let lines = vec!["First item".to_string(), "Second item".to_string()]; let result = archive_items_in_content(&lines, &[1, 2]); assert!(result.is_ok()); let (new_content, archived) = result.unwrap(); assert_eq!(new_content, ""); assert_eq!(archived, vec!["First item", "Second item"]); } /// Tests error handling for invalid archive item numbers #[test] fn test_archive_items_in_content_invalid_number() { let lines = vec!["First item".to_string()]; let result = archive_items_in_content(&lines, &[2]); assert!(result.is_err()); assert_eq!( result.unwrap_err(), "Invalid item number: 2. Valid range: 1-1" ); } /// Tests duplicate handling in archive operations #[test] fn test_archive_items_in_content_duplicates() { let lines = vec![ "First item".to_string(), "Second item".to_string(), "Third item".to_string(), ]; let result = archive_items_in_content(&lines, &[1, 1, 3]); assert!(result.is_ok()); let (new_content, archived) = result.unwrap(); assert_eq!(new_content, "Second item\n"); assert_eq!(archived, vec!["First item", "Third item"]); } /// Tests case-insensitive filtering functionality #[test] fn test_filter_functionality() { let lines = vec![ "Read: Book about Rust".to_string(), "Listen: Podcast episode".to_string(), "@work: Review PR".to_string(), "@home: Fix sink".to_string(), "Read: Another book".to_string(), ]; // Test filtering by "read:" let filtered: Vec<(usize, &String)> = lines .iter() .enumerate() .filter(|(_, line)| line.to_lowercase().contains("read:")) .collect(); assert_eq!(filtered.len(), 2); assert_eq!(filtered[0].0, 0); assert_eq!(filtered[0].1, "Read: Book about Rust"); assert_eq!(filtered[1].0, 4); assert_eq!(filtered[1].1, "Read: Another book"); // Test filtering by "@work" let filtered: Vec<(usize, &String)> = lines .iter() .enumerate() .filter(|(_, line)| line.to_lowercase().contains("@work")) .collect(); assert_eq!(filtered.len(), 1); assert_eq!(filtered[0].0, 2); assert_eq!(filtered[0].1, "@work: Review PR"); } /// Tests that filtering is case-insensitive #[test] fn test_case_insensitive_filtering() { let lines = vec![ "READ: Book about Rust".to_string(), "read: Another book".to_string(), "ReAd: Mixed case".to_string(), ]; let filtered: Vec<(usize, &String)> = lines .iter() .enumerate() .filter(|(_, line)| line.to_lowercase().contains("read:")) .collect(); assert_eq!(filtered.len(), 3); } #[cfg(test)] mod list_tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; /// Helper function to create temporary test files with content fn create_test_file_with_content(content: &str) -> NamedTempFile { let mut file = NamedTempFile::new().unwrap(); write!(file, "{}", content).unwrap(); file } /// Tests listing all items when all flag is true #[test] fn test_list_with_all_flag() { let content = "item 1\nitem 2\nitem 3\nitem 4\nitem 5\nitem 6"; let file = create_test_file_with_content(content); // Test that all=true shows all items regardless of num let result = std::panic::catch_unwind(|| { list_note(file.path(), 3, true, None).unwrap(); }); assert!(result.is_ok()); } /// Tests respecting num limit when all flag is false #[test] fn test_list_without_all_flag() { let content = "item 1\nitem 2\nitem 3\nitem 4\nitem 5\nitem 6"; let file = create_test_file_with_content(content); // Test that all=false respects num limit let result = std::panic::catch_unwind(|| { list_note(file.path(), 3, false, None).unwrap(); }); assert!(result.is_ok()); } /// Tests filtering with all flag shows all matching items #[test] fn test_list_with_filter_and_all() { let content = "read: book 1\nwrite: article\nread: book 2\ntask: cleanup"; let file = create_test_file_with_content(content); // Test that all=true with filter shows all matching items let result = std::panic::catch_unwind(|| { list_note(file.path(), 1, true, Some("read:")).unwrap(); }); assert!(result.is_ok()); } /// Tests handling of empty files #[test] fn test_list_empty_file() { let file = create_test_file_with_content(""); let result = std::panic::catch_unwind(|| { list_note(file.path(), 5, false, None).unwrap(); }); assert!(result.is_ok()); } } #[cfg(test)] mod review_tests { use std::collections::HashSet; /// Tests that prioritized items maintain their relative order during review #[test] fn test_review_prioritization_order() { // Test that items prioritized during review maintain their relative order let lines = vec![ "First item".to_string(), "Second item".to_string(), "Third item".to_string(), "Fourth item".to_string(), ]; // Simulate review process: prioritize items 2 and 4 (in that encounter order during bottom-to-top review) let mut prioritized: Vec = Vec::new(); let mut remove_indices: HashSet = HashSet::new(); // Simulate top-to-bottom review order (indices 0, 1, 2, 3) - same as list order // User prioritizes item at index 1 (Second item) prioritized.push(lines[1].clone()); remove_indices.insert(1); // User prioritizes item at index 3 (Fourth item) prioritized.push(lines[3].clone()); remove_indices.insert(3); // Build remaining items (items not prioritized) let mut remaining: Vec = Vec::new(); for (i, line) in lines.iter().enumerate() { if !remove_indices.contains(&i) { remaining.push(line.clone()); } } // Combine: prioritized items first (in order they were prioritized), then remaining let mut new_lines = prioritized; new_lines.extend(remaining); // Expected result: prioritized items (Second, Fourth) should appear first, // in the order they were prioritized, followed by remaining items (First, Third) let expected = vec![ "Second item".to_string(), "Fourth item".to_string(), "First item".to_string(), "Third item".to_string(), ]; assert_eq!(new_lines, expected); } /// Tests that quitting review still saves any changes made #[test] fn test_review_quit_saves_changes() { // Test that quitting review still saves any changes made let lines = vec![ "First item".to_string(), "Second item".to_string(), "Third item".to_string(), ]; // Simulate review process where user prioritizes one item then quits let mut prioritized: Vec = Vec::new(); let mut remove_indices: HashSet = HashSet::new(); // User prioritizes item at index 1 (Second item) prioritized.push(lines[1].clone()); remove_indices.insert(1); // User quits (but changes should still be applied) // Build remaining items (items not prioritized or archived) let mut remaining: Vec = Vec::new(); for (i, line) in lines.iter().enumerate() { if !remove_indices.contains(&i) { remaining.push(line.clone()); } } // Combine: prioritized items first, then remaining let mut new_lines = prioritized; new_lines.extend(remaining); // Expected result: prioritized item should be first, even though user quit let expected = vec![ "Second item".to_string(), "First item".to_string(), "Third item".to_string(), ]; assert_eq!(new_lines, expected); } /// Tests that items are prioritized in exact command-line order #[test] fn test_prioritize_command_line_order() { // Test that items are prioritized in the exact order specified on command line let lines = vec![ "Item A".to_string(), "Item B".to_string(), "Item C".to_string(), "Item D".to_string(), "Item E".to_string(), ]; // Command: arn up 5 2 4 (prioritize items 5, 2, 4 in that order) let result = crate::content::prioritize_items_in_content(&lines, &[5, 2, 4]); assert!(result.is_ok()); let (new_content, prioritized) = result.unwrap(); // Expected: Item E, Item B, Item D should be at top in that order let expected_content = "Item E\nItem B\nItem D\nItem A\nItem C\n"; let expected_prioritized = vec!["Item E", "Item B", "Item D"]; assert_eq!(new_content, expected_content); assert_eq!(prioritized, expected_prioritized); } /// Tests duplicate removal while preserving command-line order #[test] fn test_prioritize_command_line_order_with_duplicates() { // Test that duplicates are removed but order is preserved let lines = vec![ "Item A".to_string(), "Item B".to_string(), "Item C".to_string(), ]; // Command: arn up 3 1 3 2 1 (should become 3, 1, 2) let result = crate::content::prioritize_items_in_content(&lines, &[3, 1, 3, 2, 1]); assert!(result.is_ok()); let (new_content, prioritized) = result.unwrap(); // Expected: Item C, Item A, Item B (duplicates removed, order preserved) let expected_content = "Item C\nItem A\nItem B\n"; let expected_prioritized = vec!["Item C", "Item A", "Item B"]; assert_eq!(new_content, expected_content); assert_eq!(prioritized, expected_prioritized); } } #[cfg(test)] mod remove_tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; /// Helper function to create temporary test files with content fn create_test_file_with_content(content: &str) -> NamedTempFile { let mut file = NamedTempFile::new().unwrap(); write!(file, "{}", content).unwrap(); file } /// Tests removing a single item #[test] fn test_remove_single_item() { let content = "First item\nSecond item\nThird item\n"; let file = create_test_file_with_content(content); let result = std::panic::catch_unwind(|| { remove_items(file.path(), &[2]).unwrap(); }); assert!(result.is_ok()); // Verify the item was removed from the file let updated_content = std::fs::read_to_string(file.path()).unwrap(); assert_eq!(updated_content, "First item\nThird item\n"); } /// Tests removing multiple items #[test] fn test_remove_multiple_items() { let content = "First item\nSecond item\nThird item\nFourth item\n"; let file = create_test_file_with_content(content); let result = std::panic::catch_unwind(|| { remove_items(file.path(), &[1, 3]).unwrap(); }); assert!(result.is_ok()); // Verify the items were removed from the file let updated_content = std::fs::read_to_string(file.path()).unwrap(); assert_eq!(updated_content, "Second item\nFourth item\n"); } /// Tests removing all items results in empty file #[test] fn test_remove_all_items() { let content = "First item\nSecond item\n"; let file = create_test_file_with_content(content); let result = std::panic::catch_unwind(|| { remove_items(file.path(), &[1, 2]).unwrap(); }); assert!(result.is_ok()); // Verify the file is empty let updated_content = std::fs::read_to_string(file.path()).unwrap(); assert_eq!(updated_content, ""); } /// Tests handling of invalid item numbers #[test] fn test_remove_invalid_item_number() { let content = "First item\nSecond item\n"; let file = create_test_file_with_content(content); let result = std::panic::catch_unwind(|| { remove_items(file.path(), &[3]).unwrap(); }); assert!(result.is_ok()); // Verify the file content is unchanged let updated_content = std::fs::read_to_string(file.path()).unwrap(); assert_eq!(updated_content, content); } /// Tests handling of duplicate item numbers #[test] fn test_remove_duplicate_item_numbers() { let content = "First item\nSecond item\nThird item\n"; let file = create_test_file_with_content(content); let result = std::panic::catch_unwind(|| { remove_items(file.path(), &[2, 2, 1]).unwrap(); }); assert!(result.is_ok()); // Verify only unique items were removed let updated_content = std::fs::read_to_string(file.path()).unwrap(); assert_eq!(updated_content, "Third item\n"); } /// Tests handling of non-existent file #[test] fn test_remove_from_nonexistent_file() { let temp_dir = tempfile::tempdir().unwrap(); let nonexistent_path = temp_dir.path().join("nonexistent.txt"); let result = std::panic::catch_unwind(|| { remove_items(&nonexistent_path, &[1]).unwrap(); }); assert!(result.is_ok()); // Verify no file was created assert!(!nonexistent_path.exists()); } /// Tests handling of empty file #[test] fn test_remove_from_empty_file() { let file = create_test_file_with_content(""); let result = std::panic::catch_unwind(|| { remove_items(file.path(), &[1]).unwrap(); }); assert!(result.is_ok()); // Verify the file remains empty let updated_content = std::fs::read_to_string(file.path()).unwrap(); assert_eq!(updated_content, ""); } /// Tests that remove doesn't create an archive file (unlike do command) #[test] fn test_remove_does_not_archive() { let content = "First item\nSecond item\n"; let file = create_test_file_with_content(content); let temp_dir = tempfile::tempdir().unwrap(); let archive_path = temp_dir.path().join("archive.txt"); let result = std::panic::catch_unwind(|| { remove_items(file.path(), &[1]).unwrap(); }); assert!(result.is_ok()); // Verify no archive file was created assert!(!archive_path.exists()); // Verify the item was removed from the original file let updated_content = std::fs::read_to_string(file.path()).unwrap(); assert_eq!(updated_content, "Second item\n"); } }