"""Tests for MediaService.""" import asyncio import json import tempfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from tenacity import RetryError from src.services.media_service import ( MediaService, MediaServiceProtocol, create_media_service, ) class TestMediaService: """Test cases for MediaService.""" @pytest.fixture def media_service(self): """Create a MediaService instance for testing.""" return MediaService() @pytest.fixture def temp_dir(self): """Create a temporary directory for testing.""" with tempfile.TemporaryDirectory() as temp_dir: yield Path(temp_dir) @pytest.mark.asyncio async def test_media_service_initialization(self, media_service): """Test MediaService initialization.""" # Mock FFmpeg check to avoid actual system dependency with patch("asyncio.create_subprocess_exec") as mock_subprocess: mock_process = AsyncMock() mock_process.communicate.return_value = (b"", b"") mock_process.returncode = 0 mock_subprocess.return_value = mock_process await media_service.initialize() assert media_service.status.value == "healthy" @pytest.mark.asyncio async def test_media_service_initialization_ffmpeg_not_found(self, media_service): """Test MediaService initialization when FFmpeg is not found.""" with patch("asyncio.create_subprocess_exec") as mock_subprocess: mock_subprocess.side_effect = FileNotFoundError("ffmpeg not found") with pytest.raises(RuntimeError, match="FFmpeg not found"): await media_service.initialize() @pytest.mark.asyncio async def test_validate_file_size(self, media_service, temp_dir): """Test file size validation.""" # Create a test file test_file = temp_dir / "test.txt" test_file.write_text("test content") # Test valid file size is_valid = await media_service.validate_file_size(test_file, max_size_mb=1) assert is_valid is True # Test invalid file size (file doesn't exist) is_valid = await media_service.validate_file_size(temp_dir / "nonexistent.txt") assert is_valid is False @pytest.mark.asyncio async def test_check_audio_quality(self, media_service, temp_dir): """Test audio quality checking.""" # Mock FFprobe to return valid duration with patch("asyncio.create_subprocess_exec") as mock_subprocess: mock_process = AsyncMock() mock_process.communicate.return_value = (b"10.5", b"") # 10.5 seconds mock_process.returncode = 0 mock_subprocess.return_value = mock_process test_file = temp_dir / "test.wav" test_file.write_text("fake audio content") is_valid = await media_service.check_audio_quality(test_file) assert is_valid is True @pytest.mark.asyncio async def test_check_audio_quality_too_short(self, media_service, temp_dir): """Test audio quality checking for files that are too short.""" with patch("asyncio.create_subprocess_exec") as mock_subprocess: mock_process = AsyncMock() mock_process.communicate.return_value = (b"0.05", b"") # 0.05 seconds (too short) mock_process.returncode = 0 mock_subprocess.return_value = mock_process test_file = temp_dir / "test.wav" test_file.write_text("fake audio content") is_valid = await media_service.check_audio_quality(test_file) assert is_valid is False @pytest.mark.asyncio async def test_get_media_info(self, media_service, temp_dir): """Test getting media information.""" # Mock FFprobe to return JSON info mock_json_response = { "format": { "duration": "120.5", "format_name": "mp3", "bit_rate": "128000", "size": "1920000" }, "streams": [ { "codec_type": "audio", "codec_name": "mp3", "sample_rate": "44100", "channels": 2 } ] } with patch("asyncio.create_subprocess_exec") as mock_subprocess: mock_process = AsyncMock() mock_process.communicate.return_value = ( json.dumps(mock_json_response).encode(), b"" ) mock_process.returncode = 0 mock_subprocess.return_value = mock_process test_file = temp_dir / "test.mp3" test_file.write_text("fake audio content") info = await media_service.get_media_info(test_file) assert "duration" in info assert "mime_type" in info @pytest.mark.asyncio async def test_preprocess_audio(self, media_service, temp_dir): """Test audio preprocessing.""" input_file = temp_dir / "input.mp3" output_file = temp_dir / "output.wav" input_file.write_text("fake input audio") # Mock FFmpeg and FFprobe with patch("asyncio.create_subprocess_exec") as mock_subprocess: # Mock FFmpeg conversion mock_ffmpeg = AsyncMock() mock_ffmpeg.communicate.return_value = (b"", b"") mock_ffmpeg.returncode = 0 # Mock FFprobe for quality check mock_ffprobe = AsyncMock() mock_ffprobe.communicate.return_value = (b"10.5", b"") # Valid duration mock_ffprobe.returncode = 0 mock_subprocess.side_effect = [mock_ffmpeg, mock_ffprobe] success = await media_service.preprocess_audio(input_file, output_file) assert success is True @pytest.mark.asyncio async def test_preprocess_audio_ffmpeg_error(self, media_service, temp_dir): """Test audio preprocessing with FFmpeg error.""" input_file = temp_dir / "input.mp3" output_file = temp_dir / "output.wav" input_file.write_text("fake input audio") with patch("asyncio.create_subprocess_exec") as mock_subprocess: mock_process = AsyncMock() mock_process.communicate.return_value = (b"", b"FFmpeg error") mock_process.returncode = 1 mock_subprocess.return_value = mock_process success = await media_service.preprocess_audio(input_file, output_file) assert success is False @pytest.mark.asyncio async def test_download_media_mock(self, media_service, temp_dir): """Test media download with mocked yt-dlp.""" test_url = "https://www.youtube.com/watch?v=test123" # Mock yt-dlp mock_info = { "title": "Test Video", "filesize": 1024 * 1024, # 1MB "duration": 120, "format": "mp4" } with patch("yt_dlp.YoutubeDL") as mock_ydl_class: mock_ydl = MagicMock() mock_ydl.extract_info.return_value = mock_info mock_ydl_class.return_value.__enter__.return_value = mock_ydl # Create a fake downloaded file downloaded_file = temp_dir / "Test Video.mp4" downloaded_file.write_text("fake video content") # Mock file operations with patch.object(media_service, "get_media_info") as mock_get_info: mock_get_info.return_value = { "duration": 120.0, "mime_type": "mp4" } with patch.object(media_service, "_calculate_file_hash") as mock_hash: mock_hash.return_value = "test_hash_123" result = await media_service.download_media(test_url, temp_dir) assert result.filename == "Test Video.mp4" assert result.source_path == test_url assert result.file_hash == "test_hash_123" @pytest.mark.asyncio async def test_download_media_file_too_large(self, media_service, temp_dir): """Test media download with file size validation.""" test_url = "https://www.youtube.com/watch?v=test123" # Mock yt-dlp with large file mock_info = { "title": "Test Video", "filesize": 600 * 1024 * 1024, # 600MB (exceeds 500MB limit) "duration": 120, "format": "mp4" } with patch("yt_dlp.YoutubeDL") as mock_ydl_class: mock_ydl = MagicMock() mock_ydl.extract_info.return_value = mock_info mock_ydl_class.return_value.__enter__.return_value = mock_ydl # The retry logic will catch the ValueError and retry, but since we're mocking # the same response each time, it will eventually fail with RetryError with pytest.raises(RetryError): await media_service.download_media(test_url, temp_dir) def test_create_media_service(self): """Test factory function for creating media service.""" service = create_media_service() assert isinstance(service, MediaService) assert service.name == "media_service" # Test with custom config custom_config = {"max_file_size_mb": 1000, "retry_attempts": 5} service = create_media_service(custom_config) assert service.max_file_size_mb == 1000 assert service.retry_attempts == 5 def test_media_service_protocol(self): """Test that MediaService implements MediaServiceProtocol.""" service = MediaService() assert isinstance(service, MediaServiceProtocol) @pytest.mark.asyncio async def test_progress_hook(self, media_service): """Test progress hook functionality.""" # Test downloading progress progress_data = { "status": "downloading", "downloaded_bytes": 1024, "total_bytes": 2048, "speed": 1000, "eta": 1 } # This should not raise any exceptions media_service._progress_hook(progress_data) # Test finished status finished_data = {"status": "finished"} media_service._progress_hook(finished_data) @pytest.mark.asyncio async def test_calculate_file_hash(self, media_service, temp_dir): """Test file hash calculation.""" test_file = temp_dir / "test.txt" test_content = "test content for hashing" test_file.write_text(test_content) file_hash = await media_service._calculate_file_hash(test_file) # Should be a valid SHA-256 hash (64 characters) assert len(file_hash) == 64 assert all(c in "0123456789abcdef" for c in file_hash)