385 lines
14 KiB
Python
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 |