491 lines
19 KiB
Python
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 |