""" Unit tests for enhanced video service """ import pytest from unittest.mock import Mock, AsyncMock, patch, MagicMock from pathlib import Path from backend.models.video_download import ( DownloadMethod, DownloadPreferences, VideoDownloadResult, DownloadStatus, VideoQuality, VideoMetadata, TranscriptData, DownloaderException ) from backend.services.enhanced_video_service import EnhancedVideoService, get_enhanced_video_service from backend.config.video_download_config import VideoDownloadConfig class TestEnhancedVideoService: """Test enhanced video service functionality""" @pytest.fixture def mock_config(self, tmp_path): """Mock video download configuration""" return VideoDownloadConfig( storage_path=tmp_path, max_storage_gb=1.0, enabled_methods=[DownloadMethod.PYTUBEFIX, DownloadMethod.TRANSCRIPT_ONLY], max_video_duration_minutes=60, save_video=False ) @pytest.fixture def mock_intelligent_downloader(self): """Mock intelligent video downloader""" downloader = AsyncMock() # Mock successful result successful_result = VideoDownloadResult( video_id="test123", video_url="https://youtube.com/watch?v=test123", status=DownloadStatus.COMPLETED, method=DownloadMethod.PYTUBEFIX, audio_path=Path("/test/audio.mp3"), metadata=VideoMetadata( video_id="test123", title="Test Video", duration_seconds=240, uploader="Test Channel" ), transcript=TranscriptData( text="Test transcript content", language="en", is_auto_generated=False, segments=[{"text": "Test", "start": 0.0, "duration": 2.0}], source="youtube-transcript-api" ) ) downloader.download_video.return_value = successful_result downloader.downloaders = { DownloadMethod.TRANSCRIPT_ONLY: AsyncMock() } return downloader @pytest.fixture def service(self, mock_config, mock_intelligent_downloader): """Create enhanced video service for testing""" with patch('backend.services.enhanced_video_service.IntelligentVideoDownloader', return_value=mock_intelligent_downloader): return EnhancedVideoService(config=mock_config) def test_initialization(self, service, mock_config): """Test service initialization""" assert service.download_config == mock_config assert service.intelligent_downloader is not None def test_initialization_default_config(self): """Test initialization with default config""" with patch('backend.services.enhanced_video_service.get_video_download_config') as mock_get_config: mock_get_config.return_value = Mock() with patch('backend.services.enhanced_video_service.IntelligentVideoDownloader'): service = EnhancedVideoService() assert service.download_config is not None @pytest.mark.asyncio async def test_get_video_for_processing_success(self, service): """Test successful video processing""" url = "https://youtube.com/watch?v=test123" result = await service.get_video_for_processing(url) assert result.status == DownloadStatus.COMPLETED assert result.video_id == "test123" assert result.method == DownloadMethod.PYTUBEFIX assert result.audio_path is not None assert result.metadata is not None assert result.transcript is not None # Check that intelligent downloader was called with optimized preferences service.intelligent_downloader.download_video.assert_called_once() call_args = service.intelligent_downloader.download_video.call_args preferences = call_args[0][1] assert preferences.prefer_audio_only is True assert preferences.fallback_to_transcript is True assert preferences.extract_audio is True assert preferences.enable_subtitles is True @pytest.mark.asyncio async def test_get_video_for_processing_with_preferences(self, service): """Test video processing with custom preferences""" url = "https://youtube.com/watch?v=test123" custom_preferences = DownloadPreferences( quality=VideoQuality.HIGH_1080P, prefer_audio_only=False, save_video=True ) result = await service.get_video_for_processing(url, custom_preferences) assert result.status == DownloadStatus.COMPLETED # Verify custom preferences were used call_args = service.intelligent_downloader.download_video.call_args used_preferences = call_args[0][1] assert used_preferences.quality == VideoQuality.HIGH_1080P assert used_preferences.prefer_audio_only is False assert used_preferences.save_video is True @pytest.mark.asyncio async def test_get_video_for_processing_partial_result(self, service): """Test processing with partial result (transcript-only)""" # Mock partial result partial_result = VideoDownloadResult( video_id="partial123", video_url="https://youtube.com/watch?v=partial123", status=DownloadStatus.PARTIAL, method=DownloadMethod.TRANSCRIPT_ONLY, is_partial=True, transcript=TranscriptData( text="Partial transcript", language="en", is_auto_generated=True, segments=[], source="youtube-transcript-api" ) ) service.intelligent_downloader.download_video.return_value = partial_result url = "https://youtube.com/watch?v=partial123" result = await service.get_video_for_processing(url) assert result.status == DownloadStatus.PARTIAL assert result.is_partial is True assert result.method == DownloadMethod.TRANSCRIPT_ONLY @pytest.mark.asyncio async def test_get_video_for_processing_failure(self, service): """Test processing failure handling""" # Mock failed result failed_result = VideoDownloadResult( video_id="failed123", video_url="https://youtube.com/watch?v=failed123", status=DownloadStatus.FAILED, method=DownloadMethod.PYTUBEFIX, error_message="Download failed" ) service.intelligent_downloader.download_video.return_value = failed_result url = "https://youtube.com/watch?v=failed123" with pytest.raises(DownloaderException, match="All download methods failed"): await service.get_video_for_processing(url) @patch('backend.services.enhanced_video_service.ValidationError') @pytest.mark.asyncio async def test_validation_error_passthrough(self, mock_validation_error, service): """Test that validation errors are passed through""" # Mock validation error from parent class service.extract_video_id = Mock(side_effect=mock_validation_error("Invalid URL")) url = "invalid://not-a-youtube-url" with pytest.raises(type(mock_validation_error.return_value)): await service.get_video_for_processing(url) @pytest.mark.asyncio async def test_generic_exception_handling(self, service): """Test generic exception handling""" service.intelligent_downloader.download_video.side_effect = Exception("Unexpected error") url = "https://youtube.com/watch?v=error123" with pytest.raises(DownloaderException, match="Video processing failed"): await service.get_video_for_processing(url) @pytest.mark.asyncio async def test_get_video_metadata_only_success(self, service): """Test metadata-only extraction""" # Mock transcript downloader mock_transcript_downloader = AsyncMock() mock_metadata = VideoMetadata( video_id="meta123", title="Metadata Only Video", duration_seconds=180, uploader="Meta Channel" ) mock_transcript_downloader.get_video_metadata.return_value = mock_metadata service.intelligent_downloader.downloaders = { 'transcript_only': mock_transcript_downloader } url = "https://youtube.com/watch?v=meta123" result = await service.get_video_metadata_only(url) assert result is not None assert result['video_id'] == "meta123" assert result['title'] == "Metadata Only Video" assert result['duration_seconds'] == 180 assert result['uploader'] == "Meta Channel" @pytest.mark.asyncio async def test_get_video_metadata_only_not_found(self, service): """Test metadata extraction when not available""" # Mock transcript downloader returning None mock_transcript_downloader = AsyncMock() mock_transcript_downloader.get_video_metadata.return_value = None service.intelligent_downloader.downloaders = { 'transcript_only': mock_transcript_downloader } url = "https://youtube.com/watch?v=nometa123" result = await service.get_video_metadata_only(url) assert result is None @pytest.mark.asyncio async def test_get_video_metadata_only_no_downloader(self, service): """Test metadata extraction when transcript downloader not available""" service.intelligent_downloader.downloaders = {} url = "https://youtube.com/watch?v=nodownloader123" result = await service.get_video_metadata_only(url) assert result is None @pytest.mark.asyncio async def test_get_transcript_only_success(self, service): """Test transcript-only extraction""" # Mock transcript downloader mock_transcript_downloader = AsyncMock() mock_transcript = TranscriptData( text="Transcript only content", language="en", is_auto_generated=False, segments=[{"text": "Content", "start": 0.0, "duration": 2.0}], source="youtube-transcript-api" ) mock_transcript_downloader.get_transcript.return_value = mock_transcript service.intelligent_downloader.downloaders = { 'transcript_only': mock_transcript_downloader } url = "https://youtube.com/watch?v=transcript123" result = await service.get_transcript_only(url) assert result is not None assert result['text'] == "Transcript only content" assert result['language'] == "en" assert result['is_auto_generated'] is False assert len(result['segments']) == 1 assert result['source'] == "youtube-transcript-api" @pytest.mark.asyncio async def test_get_transcript_only_not_found(self, service): """Test transcript extraction when not available""" # Mock transcript downloader returning None mock_transcript_downloader = AsyncMock() mock_transcript_downloader.get_transcript.return_value = None service.intelligent_downloader.downloaders = { 'transcript_only': mock_transcript_downloader } url = "https://youtube.com/watch?v=notranscript123" result = await service.get_transcript_only(url) assert result is None @pytest.mark.asyncio async def test_job_status_tracking(self, service): """Test download job status tracking""" from backend.models.video_download import DownloadJobStatus from datetime import datetime # Mock job status mock_job_status = DownloadJobStatus( job_id="job123", video_url="https://youtube.com/watch?v=job123", status=DownloadStatus.IN_PROGRESS, progress_percent=50, current_method=DownloadMethod.PYTUBEFIX, created_at=datetime.now(), updated_at=datetime.now() ) service.intelligent_downloader.get_job_status.return_value = mock_job_status result = await service.get_download_job_status("job123") assert result is not None assert result['job_id'] == "job123" assert result['status'] == DownloadStatus.IN_PROGRESS.value assert result['progress_percent'] == 50 assert result['current_method'] == DownloadMethod.PYTUBEFIX.value @pytest.mark.asyncio async def test_job_status_not_found(self, service): """Test job status when job not found""" service.intelligent_downloader.get_job_status.return_value = None result = await service.get_download_job_status("notfound123") assert result is None @pytest.mark.asyncio async def test_cancel_download_success(self, service): """Test successful download cancellation""" service.intelligent_downloader.cancel_job.return_value = True result = await service.cancel_download("cancel123") assert result is True service.intelligent_downloader.cancel_job.assert_called_once_with("cancel123") @pytest.mark.asyncio async def test_cancel_download_failure(self, service): """Test download cancellation failure""" service.intelligent_downloader.cancel_job.return_value = False result = await service.cancel_download("notfound123") assert result is False @pytest.mark.asyncio async def test_get_health_status(self, service): """Test health status retrieval""" from backend.models.video_download import HealthCheckResult from datetime import datetime mock_health = HealthCheckResult( overall_status="healthy", healthy_methods=2, total_methods=2, method_details={ "pytubefix": {"status": "healthy", "last_success": True}, "transcript_only": {"status": "healthy", "last_success": True} }, recommendations=[], last_check=datetime.now() ) service.intelligent_downloader.health_check.return_value = mock_health result = await service.get_health_status() assert result['overall_status'] == "healthy" assert result['healthy_methods'] == 2 assert result['total_methods'] == 2 assert len(result['method_details']) == 2 @pytest.mark.asyncio async def test_get_download_metrics(self, service): """Test download metrics retrieval""" from backend.models.video_download import DownloadMetrics from datetime import datetime mock_metrics = DownloadMetrics( total_attempts=10, successful_downloads=8, failed_downloads=2, partial_downloads=1, method_success_rates={ "pytubefix": 0.8, "transcript_only": 1.0 }, method_attempt_counts={ "pytubefix": 9, "transcript_only": 1 }, average_download_time=45.5, average_file_size_mb=12.3, common_errors={"403 Forbidden": 2}, last_updated=datetime.now() ) service.intelligent_downloader.get_metrics.return_value = mock_metrics result = await service.get_download_metrics() assert result['total_attempts'] == 10 assert result['successful_downloads'] == 8 assert result['success_rate'] == 80.0 # (8/10) * 100 assert result['method_success_rates']['pytubefix'] == 0.8 assert result['average_download_time'] == 45.5 assert result['common_errors']['403 Forbidden'] == 2 @pytest.mark.asyncio async def test_cleanup_old_files(self, service): """Test old file cleanup""" mock_cleanup_stats = { 'files_deleted': 5, 'bytes_freed': 1024000, 'directories_cleaned': 2 } service.intelligent_downloader.cleanup_old_files.return_value = mock_cleanup_stats result = await service.cleanup_old_files(30) assert result == mock_cleanup_stats service.intelligent_downloader.cleanup_old_files.assert_called_once_with(30) def test_get_supported_methods(self, service): """Test supported methods retrieval""" service.intelligent_downloader.downloaders = { DownloadMethod.PYTUBEFIX: Mock(), DownloadMethod.TRANSCRIPT_ONLY: Mock() } result = service.get_supported_methods() assert len(result) == 2 assert DownloadMethod.PYTUBEFIX.value in result assert DownloadMethod.TRANSCRIPT_ONLY.value in result def test_get_storage_info(self, service, tmp_path): """Test storage information retrieval""" # Create mock storage structure video_dir = tmp_path / "videos" video_dir.mkdir() audio_dir = tmp_path / "audio" audio_dir.mkdir() # Create test files (video_dir / "test1.mp4").write_text("video content 1") (video_dir / "test2.mp4").write_text("video content 2") (audio_dir / "test1.mp3").write_text("audio content") # Mock storage directories service.download_config.get_storage_dirs.return_value = { 'videos': video_dir, 'audio': audio_dir } result = service.get_storage_info() assert 'videos' in result assert 'audio' in result assert 'total' in result # Check video directory info assert result['videos']['exists'] is True assert result['videos']['file_count'] == 2 assert result['videos']['size_bytes'] > 0 # Check audio directory info assert result['audio']['exists'] is True assert result['audio']['file_count'] == 1 # Check total usage assert result['total']['size_bytes'] > 0 assert result['total']['usage_percent'] >= 0 def test_get_enhanced_video_service_dependency(): """Test dependency injection function""" with patch('backend.services.enhanced_video_service.EnhancedVideoService') as mock_service: result = get_enhanced_video_service() mock_service.assert_called_once() assert result == mock_service.return_value