""" Unit tests for VideoDownloadService. Tests download, storage, caching, and cleanup functionality. """ import pytest import asyncio from unittest.mock import Mock, AsyncMock, patch, MagicMock from pathlib import Path import json import tempfile import shutil from datetime import datetime import sys sys.path.insert(0, '/Users/enias/projects/my-ai-projects/apps/youtube-summarizer') from backend.services.video_download_service import VideoDownloadService, VideoDownloadError class TestVideoDownloadService: """Test suite for VideoDownloadService.""" @pytest.fixture def temp_storage(self): """Create temporary storage directory.""" temp_dir = tempfile.mkdtemp() yield temp_dir shutil.rmtree(temp_dir, ignore_errors=True) @pytest.fixture def video_service(self, temp_storage): """Create VideoDownloadService with temp storage.""" return VideoDownloadService( storage_dir=temp_storage, max_storage_size_gb=0.001, # 1MB for testing video_quality="720p" ) @pytest.fixture def mock_video_info(self): """Mock video information.""" return { 'id': 'test_video_123', 'title': 'Test Video Title', 'channel': 'Test Channel', 'duration': 180, 'filesize_approx': 1024 * 500, # 500KB 'description': 'Test video description', 'uploader': 'Test Uploader' } def test_init_creates_directories(self, temp_storage): """Test that initialization creates required directories.""" service = VideoDownloadService(storage_dir=temp_storage) assert (Path(temp_storage) / "videos").exists() assert (Path(temp_storage) / "audio").exists() assert (Path(temp_storage) / "temp").exists() assert (Path(temp_storage) / "metadata").exists() assert (Path(temp_storage) / "download_cache.json").exists() def test_video_hash_generation(self, video_service): """Test video hash generation is consistent.""" video_id = "test_video_123" hash1 = video_service._get_video_hash(video_id) hash2 = video_service._get_video_hash(video_id) assert hash1 == hash2 assert len(hash1) == 32 # MD5 hash length def test_cache_save_and_load(self, video_service): """Test cache persistence.""" test_cache = { 'test_hash': { 'video_id': 'test_123', 'title': 'Test Video', 'video_path': '/path/to/video.mp4' } } video_service.cache = test_cache video_service._save_cache() # Create new service to test loading new_service = VideoDownloadService( storage_dir=video_service.base_dir ) assert new_service.cache == test_cache def test_is_video_downloaded(self, video_service): """Test checking if video is downloaded.""" video_id = "test_video_123" video_hash = video_service._get_video_hash(video_id) # Not downloaded initially assert not video_service.is_video_downloaded(video_id) # Create fake video file video_path = video_service.videos_dir / f"{video_id}.mp4" video_path.touch() # Add to cache video_service.cache[video_hash] = { 'video_id': video_id, 'video_path': str(video_path) } assert video_service.is_video_downloaded(video_id) def test_storage_usage_calculation(self, video_service): """Test storage usage calculation.""" # Create some test files video_file = video_service.videos_dir / "test.mp4" audio_file = video_service.audio_dir / "test.mp3" video_file.write_text("x" * 1000) # 1KB audio_file.write_text("x" * 500) # 500 bytes usage = video_service.get_current_storage_usage() assert usage == 1500 # 1000 + 500 bytes def test_cleanup_old_videos(self, video_service): """Test cleanup of old videos.""" # Create test video files old_video = video_service.videos_dir / "old.mp4" old_audio = video_service.audio_dir / "old.mp3" new_video = video_service.videos_dir / "new.mp4" old_video.write_text("x" * 1000) old_audio.write_text("x" * 500) new_video.write_text("x" * 800) # Add to cache with different dates video_service.cache = { 'old_hash': { 'video_id': 'old_video', 'video_path': str(old_video), 'audio_path': str(old_audio), 'download_date': '2024-01-01T00:00:00', 'keep': False }, 'new_hash': { 'video_id': 'new_video', 'video_path': str(new_video), 'download_date': '2024-12-01T00:00:00', 'keep': False } } # Clean up 1500 bytes (should remove old video and audio) freed = video_service.cleanup_old_videos(1500) assert freed == 1500 assert not old_video.exists() assert not old_audio.exists() assert new_video.exists() assert 'old_hash' not in video_service.cache assert 'new_hash' in video_service.cache def test_cleanup_respects_keep_flag(self, video_service): """Test that cleanup respects the 'keep' flag.""" keep_video = video_service.videos_dir / "keep.mp4" keep_video.write_text("x" * 1000) video_service.cache = { 'keep_hash': { 'video_id': 'keep_video', 'video_path': str(keep_video), 'download_date': '2024-01-01T00:00:00', 'keep': True # Should not be deleted } } freed = video_service.cleanup_old_videos(1000) assert freed == 0 assert keep_video.exists() assert 'keep_hash' in video_service.cache @pytest.mark.asyncio async def test_get_video_info(self, video_service, mock_video_info): """Test getting video information.""" with patch.object(video_service, 'get_video_info') as mock_get_info: mock_get_info.return_value = mock_video_info info = await video_service.get_video_info("https://youtube.com/watch?v=test123") assert info['id'] == 'test_video_123' assert info['title'] == 'Test Video Title' assert info['duration'] == 180 @pytest.mark.asyncio async def test_download_video_success(self, video_service, mock_video_info): """Test successful video download.""" url = "https://youtube.com/watch?v=test123" with patch.object(video_service, 'get_video_info') as mock_get_info: mock_get_info.return_value = mock_video_info with patch('yt_dlp.YoutubeDL') as mock_ydl_class: mock_ydl = MagicMock() mock_ydl_class.return_value.__enter__.return_value = mock_ydl # Create fake video file when download is called def create_video(*args): video_path = video_service.videos_dir / "test_video_123.mp4" video_path.write_text("fake video content") audio_path = video_service.audio_dir / "test_video_123.mp3" audio_path.write_text("fake audio") return None mock_ydl.download.side_effect = create_video video_path, audio_path = await video_service.download_video( url, extract_audio=True ) assert video_path.exists() assert audio_path.exists() assert video_service.is_video_downloaded('test_video_123') @pytest.mark.asyncio async def test_download_video_cached(self, video_service, mock_video_info): """Test that cached videos are not re-downloaded.""" url = "https://youtube.com/watch?v=test123" video_id = mock_video_info['id'] # Create existing files video_path = video_service.videos_dir / f"{video_id}.mp4" audio_path = video_service.audio_dir / f"{video_id}.mp3" video_path.touch() audio_path.touch() # Add to cache video_hash = video_service._get_video_hash(video_id) video_service.cache[video_hash] = { 'video_id': video_id, 'video_path': str(video_path), 'audio_path': str(audio_path) } with patch.object(video_service, 'get_video_info') as mock_get_info: mock_get_info.return_value = mock_video_info with patch('yt_dlp.YoutubeDL') as mock_ydl_class: result_video, result_audio = await video_service.download_video( url, extract_audio=True, force=False ) # Should not call download since it's cached mock_ydl_class.assert_not_called() assert result_video == video_path assert result_audio == audio_path @pytest.mark.asyncio async def test_download_video_storage_limit(self, video_service, mock_video_info): """Test download with storage limit enforcement.""" url = "https://youtube.com/watch?v=test123" # Set very small storage limit video_service.max_storage_bytes = 100 # 100 bytes mock_video_info['filesize_approx'] = 200 # Larger than limit with patch.object(video_service, 'get_video_info') as mock_get_info: mock_get_info.return_value = mock_video_info with patch.object(video_service, 'cleanup_old_videos') as mock_cleanup: mock_cleanup.return_value = 50 # Not enough freed with pytest.raises(VideoDownloadError) as exc_info: await video_service.download_video(url) assert "Insufficient storage space" in str(exc_info.value) def test_progress_hook(self, video_service): """Test download progress tracking.""" video_id = "test_123" # Simulate downloading progress progress_data = { 'status': 'downloading', '_percent_str': '50.0%', '_speed_str': '1.5MB/s', '_eta_str': '00:30', 'total_bytes': 1000000, 'downloaded_bytes': 500000 } video_service._progress_hook(video_id, progress_data) progress = video_service.download_progress[video_id] assert progress['status'] == 'downloading' assert progress['percent'] == '50.0%' assert progress['speed'] == '1.5MB/s' assert progress['eta'] == '00:30' # Simulate finished finished_data = {'status': 'finished'} video_service._progress_hook(video_id, finished_data) progress = video_service.download_progress[video_id] assert progress['status'] == 'finished' assert progress['percent'] == '100%' def test_cleanup_failed_download(self, video_service): """Test cleanup of failed download.""" video_id = "failed_video" video_hash = video_service._get_video_hash(video_id) # Create files video_path = video_service.videos_dir / f"{video_id}.mp4" audio_path = video_service.audio_dir / f"{video_id}.mp3" info_path = video_service.videos_dir / f"{video_id}.info.json" thumb_path = video_service.videos_dir / f"{video_id}.jpg" for path in [video_path, audio_path, info_path, thumb_path]: path.touch() # Add to cache video_service.cache[video_hash] = { 'video_id': video_id, 'video_path': str(video_path), 'audio_path': str(audio_path) } video_service._cleanup_failed_download(video_id) # All files should be removed assert not video_path.exists() assert not audio_path.exists() assert not info_path.exists() assert not thumb_path.exists() assert video_hash not in video_service.cache def test_get_storage_stats(self, video_service): """Test storage statistics generation.""" # Add some videos to cache video_service.cache = { 'hash1': {'video_id': 'video1'}, 'hash2': {'video_id': 'video2'} } # Create some files (video_service.videos_dir / "test.mp4").write_text("x" * 1024) (video_service.audio_dir / "test.mp3").write_text("x" * 512) stats = video_service.get_storage_stats() assert stats['total_videos'] == 2 assert stats['total_size_bytes'] == 1536 assert stats['total_size_mb'] == pytest.approx(0.00146, rel=0.01) assert stats['video_quality'] == '720p' assert stats['keep_videos'] == True assert stats['usage_percent'] >= 0 def test_get_cached_videos(self, video_service): """Test getting list of cached videos.""" video_path = video_service.videos_dir / "test.mp4" video_path.touch() video_service.cache = { 'hash1': { 'video_id': 'video1', 'title': 'Video 1', 'video_path': str(video_path), 'download_date': '2024-01-01T00:00:00' }, 'hash2': { 'video_id': 'video2', 'title': 'Video 2', 'video_path': '/nonexistent/path.mp4', 'download_date': '2024-01-02T00:00:00' } } videos = video_service.get_cached_videos() assert len(videos) == 2 assert videos[0]['video_id'] == 'video2' # Newer first assert videos[1]['video_id'] == 'video1' assert videos[0]['exists'] == False assert videos[1]['exists'] == True