"""Tests for the service layer. Tests for FileTreeService, MetadataService, and RenameService. """ import pytest from pathlib import Path from unittest.mock import Mock, MagicMock, patch import tempfile import os from renamer.services import FileTreeService, MetadataService, RenameService from renamer.cache import Cache from renamer.settings import Settings class TestFileTreeService: """Test FileTreeService functionality.""" @pytest.fixture def service(self): """Create a FileTreeService instance.""" return FileTreeService() @pytest.fixture def temp_dir(self): """Create a temporary directory with test files.""" with tempfile.TemporaryDirectory() as tmpdir: tmpdir = Path(tmpdir) # Create some test files (tmpdir / "movie1.mkv").touch() (tmpdir / "movie2.mp4").touch() (tmpdir / "readme.txt").touch() # Create subdirectory subdir = tmpdir / "subdir" subdir.mkdir() (subdir / "movie3.avi").touch() yield tmpdir def test_validate_directory_valid(self, service, temp_dir): """Test validating a valid directory.""" is_valid, error = service.validate_directory(temp_dir) assert is_valid is True assert error is None def test_validate_directory_not_exists(self, service): """Test validating a non-existent directory.""" is_valid, error = service.validate_directory(Path("/nonexistent")) assert is_valid is False assert "does not exist" in error def test_validate_directory_is_file(self, service, temp_dir): """Test validating a file instead of directory.""" file_path = temp_dir / "movie1.mkv" is_valid, error = service.validate_directory(file_path) assert is_valid is False assert "not a directory" in error def test_scan_directory(self, service, temp_dir): """Test scanning directory for media files.""" files = service.scan_directory(temp_dir) # Should find 3 media files (2 in root, 1 in subdir) assert len(files) == 3 # Check file types extensions = {f.suffix for f in files} assert extensions == {'.mkv', '.mp4', '.avi'} def test_scan_directory_non_recursive(self, service, temp_dir): """Test scanning without recursion.""" files = service.scan_directory(temp_dir, recursive=False) # Should only find 2 files in root (not subdir) assert len(files) == 2 def test_is_media_file(self, service): """Test media file detection.""" assert service._is_media_file(Path("movie.mkv")) is True assert service._is_media_file(Path("movie.mp4")) is True assert service._is_media_file(Path("readme.txt")) is False assert service._is_media_file(Path("movie.MKV")) is True # Case insensitive def test_count_media_files(self, service, temp_dir): """Test counting media files.""" count = service.count_media_files(temp_dir) assert count == 3 def test_get_directory_stats(self, service, temp_dir): """Test getting directory statistics.""" stats = service.get_directory_stats(temp_dir) assert stats['total_files'] == 4 # 3 media + 1 txt assert stats['total_dirs'] == 1 # 1 subdir assert stats['media_files'] == 3 class TestMetadataService: """Test MetadataService functionality.""" @pytest.fixture def cache(self): """Create a cache instance.""" return Cache() @pytest.fixture def settings(self): """Create a settings instance.""" return Settings() @pytest.fixture def service(self, cache, settings): """Create a MetadataService instance.""" return MetadataService(cache, settings, max_workers=2) @pytest.fixture def test_file(self): """Create a temporary test file.""" with tempfile.NamedTemporaryFile(suffix='.mkv', delete=False) as f: path = Path(f.name) yield path # Cleanup if path.exists(): path.unlink() def test_service_initialization(self, service): """Test service initializes correctly.""" assert service.max_workers == 2 assert service.executor is not None assert service._lock is not None def test_extract_metadata_sync(self, service, test_file): """Test synchronous metadata extraction.""" result = service.extract_metadata(test_file) assert result is not None assert 'formatted_info' in result assert 'proposed_name' in result assert 'mode' in result def test_extract_metadata_async(self, service, test_file): """Test asynchronous metadata extraction with callback.""" callback_result = None def callback(result): nonlocal callback_result callback_result = result service.extract_metadata(test_file, callback=callback) # Wait for async operation import time time.sleep(1.0) # Callback should have been called # May be None if file doesn't exist or extraction failed assert callback_result is None or 'formatted_info' in callback_result def test_get_active_extraction_count(self, service): """Test getting active extraction count.""" count = service.get_active_extraction_count() assert count == 0 def test_shutdown(self, service): """Test service shutdown.""" service.shutdown(wait=False) # Should not raise any errors def test_context_manager(self, cache, settings): """Test using service as context manager.""" with MetadataService(cache, settings) as service: assert service.executor is not None # Executor should be shut down after context class TestRenameService: """Test RenameService functionality.""" @pytest.fixture def service(self): """Create a RenameService instance.""" return RenameService() @pytest.fixture def test_file(self): """Create a temporary test file.""" with tempfile.NamedTemporaryFile(suffix='.mkv', delete=False) as f: path = Path(f.name) yield path # Cleanup if path.exists(): path.unlink() def test_sanitize_filename(self, service): """Test filename sanitization.""" assert service.sanitize_filename("Movie: Title?") == "Movie Title" assert service.sanitize_filename("Movie<>|*.mkv") == "Movie.mkv" assert service.sanitize_filename(" Movie ") == "Movie" assert service.sanitize_filename("Movie...") == "Movie" def test_validate_filename_valid(self, service): """Test validating a valid filename.""" is_valid, error = service.validate_filename("movie.mkv") assert is_valid is True assert error is None def test_validate_filename_empty(self, service): """Test validating empty filename.""" is_valid, error = service.validate_filename("") assert is_valid is False assert "empty" in error.lower() def test_validate_filename_too_long(self, service): """Test validating too long filename.""" long_name = "a" * 300 is_valid, error = service.validate_filename(long_name) assert is_valid is False assert "too long" in error.lower() def test_validate_filename_reserved(self, service): """Test validating reserved Windows names.""" is_valid, error = service.validate_filename("CON.txt") assert is_valid is False assert "reserved" in error.lower() def test_validate_filename_invalid_chars(self, service): """Test validating filename with invalid characters.""" is_valid, error = service.validate_filename("movie<>.mkv") assert is_valid is False assert "invalid" in error.lower() def test_check_name_conflict_no_conflict(self, service, test_file): """Test checking for name conflict when none exists.""" has_conflict, msg = service.check_name_conflict(test_file, "newname.mkv") assert has_conflict is False assert msg is None def test_check_name_conflict_exists(self, service, test_file): """Test checking for name conflict when file exists.""" # Use the same filename has_conflict, msg = service.check_name_conflict(test_file, test_file.name) assert has_conflict is False # Same file, no conflict # Create another file other_file = test_file.parent / "other.mkv" other_file.touch() has_conflict, msg = service.check_name_conflict(test_file, "other.mkv") assert has_conflict is True assert "already exists" in msg # Cleanup other_file.unlink() def test_rename_file_dry_run(self, service, test_file): """Test renaming file in dry-run mode.""" success, msg = service.rename_file(test_file, "newname.mkv", dry_run=True) assert success is True assert "Would rename" in msg # File should not actually be renamed assert test_file.exists() def test_rename_file_actual(self, service, test_file): """Test actually renaming a file.""" old_name = test_file.name new_name = "renamed.mkv" success, msg = service.rename_file(test_file, new_name, dry_run=False) assert success is True assert "Renamed" in msg # Check file was renamed new_path = test_file.parent / new_name assert new_path.exists() assert not test_file.exists() # Cleanup new_path.unlink() def test_rename_file_not_exists(self, service): """Test renaming a file that doesn't exist.""" fake_path = Path("/nonexistent/file.mkv") success, msg = service.rename_file(fake_path, "new.mkv") assert success is False assert "does not exist" in msg def test_strip_markup(self, service): """Test stripping markup tags.""" assert service._strip_markup("[bold]text[/bold]") == "text" assert service._strip_markup("[green]Movie[/green]") == "Movie" assert service._strip_markup("No markup") == "No markup" assert service._strip_markup("[bold green]text[/bold green]") == "text" class TestServiceIntegration: """Integration tests for services working together.""" @pytest.fixture def temp_dir(self): """Create a temporary directory with test files.""" with tempfile.TemporaryDirectory() as tmpdir: tmpdir = Path(tmpdir) (tmpdir / "movie1.mkv").touch() (tmpdir / "movie2.mp4").touch() yield tmpdir def test_scan_and_rename_workflow(self, temp_dir): """Test a complete workflow: scan, then rename.""" # Scan for files tree_service = FileTreeService() files = tree_service.scan_directory(temp_dir) assert len(files) == 2 # Rename one file rename_service = RenameService() old_file = files[0] success, msg = rename_service.rename_file(old_file, "renamed.mkv") assert success is True # Scan again new_files = tree_service.scan_directory(temp_dir) assert len(new_files) == 2 # Check renamed file exists renamed_path = temp_dir / "renamed.mkv" assert renamed_path.exists()