- Implement core Rust backend with FFmpeg integration - Add TheTVDB API client with token caching - Implement directory scanner with progress callbacks - Create file manager with rename and move operations - Add audit logging functionality - Implement file mapping for TV episode renaming - Build Node.js native addon via NAPI - Include comprehensive unit and integration tests - Update gitignore to exclude build artifacts and temp files Resolves #TBD
391 lines
14 KiB
Rust
391 lines
14 KiB
Rust
// 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));
|
|
}
|