// End-to-end tests for MovieMapper // Tests complete workflows from start to finish use movie_mapper::service::audit_logger::{AuditAction, AuditLogger}; use movie_mapper::service::file_mapper::FileMapper; use movie_mapper::service::file_metadata::MetadataExtractor; use movie_mapper::service::file_scanner::FileScanner; use movie_mapper::service::tag_manager::TagManager; use std::fs::{self, File}; use std::io::Write; use tempfile::tempdir; /// Test complete scanning workflow #[tokio::test] async fn test_complete_scanning_workflow() { let temp_dir = tempdir().unwrap(); // Setup: Create a directory structure with folders and media files let tv_dir = temp_dir.path().join("TV Shows"); let movies_dir = temp_dir.path().join("Movies"); fs::create_dir(&tv_dir).unwrap(); fs::create_dir(&movies_dir).unwrap(); // Create TV show folders let breaking_bad_dir = tv_dir.join("Breaking Bad (2008)"); fs::create_dir(&breaking_bad_dir).unwrap(); // Create some test media files in each directory File::create(breaking_bad_dir.join("S01E01 - Pilot.mp4")).unwrap(); File::create(breaking_bad_dir.join("S01E02 - Cat's Cradle.mp4")).unwrap(); // Create movie files File::create(movies_dir.join("Inception (2010).mp4")).unwrap(); File::create(movies_dir.join("The Matrix (1999).mkv")).unwrap(); // Execute: Scan the root directory let scanner = FileScanner::new(); let files = scanner.scan_directory(temp_dir.path(), None).await.unwrap(); // Assert: Verify directory structure was captured (non-recursive scan) assert_eq!(files.len(), 2); // 2 folders only (non-recursive) assert!(files.iter().any(|f| f.name == "TV Shows" && f.is_folder)); assert!(files.iter().any(|f| f.name == "Movies" && f.is_folder)); } /// Test complete mapping workflow with Jellyfin naming #[tokio::test] async fn test_complete_mapping_workflow() { let temp_dir = tempdir().unwrap(); // Setup: Create files with different qualities let files_data = vec![ ("video1.mp4", "1080p"), ("video2.mkv", "720p"), ("video3.mp4", "480p"), ]; for (filename, quality) in &files_data { let file_path = temp_dir.path().join(filename); let mut file = File::create(&file_path).unwrap(); file.write_all(b"test data").unwrap(); // Create MediaFile with quality metadata let mut media_file = movie_mapper::model::file::MediaFile::from_path(file_path); media_file.quality = quality.to_string(); // Save the modified file back to the directory } // Execute: Map files to Jellyfin naming let mapper = FileMapper::new(); // Note: In a real scenario, we'd need to actually process the files // For this test, we'll verify the mapping logic works let filename = mapper.generate_jellyfin_filename("Test Show", 1, 1, 1, "1080p", ".mp4"); // Assert: Verify Jellyfin naming convention assert_eq!(filename, "Test Show S01E01 - 1080p.mp4"); } /// Test complete tag and move workflow #[tokio::test] async fn test_complete_tag_and_move_workflow() { let temp_dir = tempdir().unwrap(); let extras_folder = temp_dir.path().join("extras"); let commentary_folder = temp_dir.path().join("commentary"); // Setup: Create test files let files = vec![ ("movie1.mp4", vec!["extra"]), ("movie2.mkv", vec!["extra", "commentary"]), ("movie3.mp4", vec!["commentary"]), ("movie4.mkv", vec![]), // No tags ]; for (filename, tags) in &files { let file_path = temp_dir.path().join(filename); File::create(&file_path).unwrap(); for tag in tags { // Simulate tagging by creating a tracking file let tag_file = temp_dir.path().join(format!(".tag_{}_{}", tag, filename)); File::create(&tag_file).unwrap(); } } // Execute: Tag and move files let mut tag_manager = TagManager::new(); // Tag files for (filename, tags) in &files { let file_path = temp_dir.path().join(filename); for tag in tags { tag_manager.add_tag(&file_path, tag).unwrap(); } } // Move extra files fs::create_dir_all(&extras_folder).unwrap(); let moved_extras = tag_manager .move_tagged_files("extra", &extras_folder) .await .unwrap(); // Move commentary files fs::create_dir_all(&commentary_folder).unwrap(); let moved_commentary = tag_manager .move_tagged_files("commentary", &commentary_folder) .await .unwrap(); // Assert: Verify files were moved correctly // movie1.mp4 has only "extra" -> moved to extras (1 file) // movie2.mkv has both "extra" and "commentary" -> moved to extras first, then tried to move to commentary (but source no longer exists) // movie3.mp4 has only "commentary" -> moved to commentary (1 file) assert_eq!(moved_extras, 2); // movie1.mp4 and movie2.mkv had 'extra' tag assert_eq!(moved_commentary, 1); // only movie3.mp4 was still available to move to commentary // Verify files were moved to correct folders assert!(extras_folder.join("movie1.mp4").exists()); assert!(extras_folder.join("movie2.mkv").exists()); // movie2.mkv is only in extras (not commentary) because it was moved to extras first // When we try to move it to commentary, it's no longer at the original path assert!(commentary_folder.join("movie3.mp4").exists()); // movie1.mp4 was moved to extras (original path doesn't exist) let movie1_path = temp_dir.path().join("movie1.mp4"); assert!(!movie1_path.exists()); // movie2.mkv was moved to extras (and when we tried to move it to commentary, it was already gone) // So movie2.mkv only ends up in extras, not in commentary assert!(!temp_dir.path().join("movie2.mkv").exists()); // movie3.mp4 was moved to commentary assert!(!temp_dir.path().join("movie3.mp4").exists()); // movie4.mkv has no tags, so it should still be at the original location assert!(temp_dir.path().join("movie4.mkv").exists()); } /// Test complete workflow with audit logging #[tokio::test] async fn test_complete_workflow_with_audit() { let temp_dir = tempdir().unwrap(); let logger = AuditLogger::new(temp_dir.path().to_str().unwrap()); // Setup: Create test files let file_path = temp_dir.path().join("test.mp4"); File::create(&file_path).unwrap(); // Step 1: Select directory let directory_action = AuditAction::DirectorySelected { path: temp_dir.path().to_string_lossy().to_string(), }; logger.log_event(directory_action).unwrap(); // Step 2: Tag file let tag_action = AuditAction::TagFile { file_path: file_path.to_string_lossy().to_string(), tag: "extra".to_string(), }; logger.log_event(tag_action).unwrap(); // Step 3: Move file let extras_folder = temp_dir.path().join("extras"); fs::create_dir_all(&extras_folder).unwrap(); let new_path = extras_folder.join("test.mp4"); fs::rename(&file_path, &new_path).unwrap(); let move_action = AuditAction::MoveFile { original_path: file_path.to_string_lossy().to_string(), new_path: new_path.to_string_lossy().to_string(), folder: "extras".to_string(), }; logger.log_event(move_action).unwrap(); // Assert: Verify audit log let audit_path = temp_dir.path().join(".audit"); assert!(audit_path.exists()); let content = fs::read_to_string(&audit_path).unwrap(); assert!(content.contains("directory_selected")); assert!(content.contains("tag_file")); assert!(content.contains("move_file")); assert_eq!(content.matches("directory_selected").count(), 1); assert_eq!(content.matches("tag_file").count(), 1); assert_eq!(content.matches("move_file").count(), 1); } /// Test error handling in complete workflow #[tokio::test] async fn test_workflow_error_handling() { let scanner = FileScanner::new(); // Test 1: Scan non-existent directory let result = scanner .scan_directory(std::path::Path::new("/nonexistent/path"), None) .await; assert!(result.is_err(), "Should return error for non-existent path"); // Test 2: Extract metadata from non-existent file let extractor = MetadataExtractor::new(); let result = extractor.extract_metadata(std::path::Path::new("/nonexistent/file.mp4")); assert!(result.is_ok(), "Should not panic, just return defaults"); let file = result.unwrap(); assert_eq!(file.duration, "00:00"); assert_eq!(file.quality, "unknown"); // Test 3: Move non-existent tagged file let temp_dir = tempdir().unwrap(); let target_folder = temp_dir.path().join("target"); let mut tag_manager = TagManager::new(); let fake_path = temp_dir.path().join("fake.mp4"); tag_manager.add_tag(&fake_path, "extra").unwrap(); // This should not fail even though file doesn't exist let result = tag_manager.move_tagged_files("extra", &target_folder).await; // Should return 0 moved (files that don't exist are just skipped) assert!(result.is_ok()); } /// Test TVDB integration end-to-end (requires API key) #[tokio::test] async fn test_tvdb_integration_e2e() { let api_key = match std::env::var("TVDB_API_KEY") { Ok(key) => key, Err(_) => { // Skip test if no API key return; } }; let mut tvdb = movie_mapper::service::tvdb_api::TVDBClient::new(&api_key).unwrap(); // Step 1: Authenticate tvdb.authenticate().await.unwrap(); // Step 2: Search for shows let shows = tvdb.search("Breaking Bad").await.unwrap(); assert!(!shows.is_empty(), "Should find at least one show"); // Step 3: Get details for first show let show_id = shows[0].id; let details = tvdb.get_show_details(show_id).await.unwrap(); assert_eq!(details.id, show_id); assert!(!details.name.is_empty()); assert!(!details.overview.is_empty()); } /// Test scanning with hidden files (should be ignored) #[tokio::test] async fn test_scan_ignores_hidden_files() { let temp_dir = tempdir().unwrap(); // Create visible and hidden files File::create(temp_dir.path().join("visible.mp4")).unwrap(); File::create(temp_dir.path().join(".hidden.mp4")).unwrap(); File::create(temp_dir.path().join(".DS_Store")).unwrap(); let scanner = FileScanner::new(); let files = scanner.scan_directory(temp_dir.path(), None).await.unwrap(); // Only visible file should be in results assert_eq!(files.len(), 1); assert_eq!(files[0].name, "visible.mp4"); } /// Test scanning with various media extensions #[tokio::test] async fn test_scan_recognizes_all_media_extensions() { let temp_dir = tempdir().unwrap(); // Create files with different extensions let extensions = [".mp4", ".mkv", ".avi", ".mov", ".flv", ".webm"]; for ext in &extensions { File::create(temp_dir.path().join(format!("video{}", ext))).unwrap(); } let scanner = FileScanner::new(); let files = scanner.scan_directory(temp_dir.path(), None).await.unwrap(); // All media files should be detected assert_eq!(files.len(), 6); for file in &files { assert!(extensions.contains(&format!(".{}", file.name.split('.').next_back().unwrap()).as_str())); } } /// Test scanning with non-media files (should be ignored) #[tokio::test] async fn test_scan_ignores_non_media_files() { let temp_dir = tempdir().unwrap(); // Create media and non-media files File::create(temp_dir.path().join("movie.mp4")).unwrap(); File::create(temp_dir.path().join("poster.jpg")).unwrap(); File::create(temp_dir.path().join("subtitle.srt")).unwrap(); File::create(temp_dir.path().join("metadata.xml")).unwrap(); let scanner = FileScanner::new(); let files = scanner.scan_directory(temp_dir.path(), None).await.unwrap(); // Only media file should be detected assert_eq!(files.len(), 1); assert_eq!(files[0].name, "movie.mp4"); } /// Test scanning with special characters in filenames #[tokio::test] async fn test_scan_with_special_characters() { let temp_dir = tempdir().unwrap(); // Create files with special characters (common in media) let filenames = [ "Movie (2024).mp4", "Movie - Director's Cut.mp4", "Movie.S01E01.1080p.mp4", "Movie Name 2024.mp4", ]; for filename in &filenames { File::create(temp_dir.path().join(filename)).unwrap(); } let scanner = FileScanner::new(); let files = scanner.scan_directory(temp_dir.path(), None).await.unwrap(); // All files should be detected (files are sorted alphabetically) assert_eq!(files.len(), 4); let file_names: Vec<&str> = files.iter().map(|f| f.name.as_str()).collect(); // Check that all expected filenames are present (order may vary due to sorting) for filename in &filenames { assert!(file_names.contains(filename)); } } /// Test scanning with permission denied scenario #[tokio::test] async fn test_scan_with_permission_denied() { let temp_dir = tempdir().unwrap(); // Create a subdirectory let sub_dir = temp_dir.path().join("subdir"); fs::create_dir(&sub_dir).unwrap(); // Create a file in the subdirectory File::create(sub_dir.join("video.mp4")).unwrap(); // Create a file in the parent directory File::create(temp_dir.path().join("video.mp4")).unwrap(); let scanner = FileScanner::new(); let files = scanner.scan_directory(temp_dir.path(), None).await.unwrap(); // Should only see the parent directory contents, not subdirectories // (non-recursive scan) - should find 2 items: 1 folder (subdir) + 1 file (video.mp4) assert_eq!( files.len(), 2, "Expected 2 items (1 folder + 1 file) but found {}", files.len() ); assert!(files.iter().any(|f| f.name == "video.mp4" && !f.is_folder)); assert!(files.iter().any(|f| f.name == "subdir" && f.is_folder)); }