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

385 lines
14 KiB
Python

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