youtube-summarizer/backend/tests/unit/test_enhanced_video_service.py

491 lines
19 KiB
Python

"""
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