287 lines
11 KiB
Python
287 lines
11 KiB
Python
"""Tests for MediaService."""
|
|
|
|
import asyncio
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from tenacity import RetryError
|
|
|
|
from src.services.media_service import (
|
|
MediaService,
|
|
MediaServiceProtocol,
|
|
create_media_service,
|
|
)
|
|
|
|
|
|
class TestMediaService:
|
|
"""Test cases for MediaService."""
|
|
|
|
@pytest.fixture
|
|
def media_service(self):
|
|
"""Create a MediaService instance for testing."""
|
|
return MediaService()
|
|
|
|
@pytest.fixture
|
|
def temp_dir(self):
|
|
"""Create a temporary directory for testing."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
yield Path(temp_dir)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_media_service_initialization(self, media_service):
|
|
"""Test MediaService initialization."""
|
|
# Mock FFmpeg check to avoid actual system dependency
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
mock_process = AsyncMock()
|
|
mock_process.communicate.return_value = (b"", b"")
|
|
mock_process.returncode = 0
|
|
mock_subprocess.return_value = mock_process
|
|
|
|
await media_service.initialize()
|
|
assert media_service.status.value == "healthy"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_media_service_initialization_ffmpeg_not_found(self, media_service):
|
|
"""Test MediaService initialization when FFmpeg is not found."""
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
mock_subprocess.side_effect = FileNotFoundError("ffmpeg not found")
|
|
|
|
with pytest.raises(RuntimeError, match="FFmpeg not found"):
|
|
await media_service.initialize()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_file_size(self, media_service, temp_dir):
|
|
"""Test file size validation."""
|
|
# Create a test file
|
|
test_file = temp_dir / "test.txt"
|
|
test_file.write_text("test content")
|
|
|
|
# Test valid file size
|
|
is_valid = await media_service.validate_file_size(test_file, max_size_mb=1)
|
|
assert is_valid is True
|
|
|
|
# Test invalid file size (file doesn't exist)
|
|
is_valid = await media_service.validate_file_size(temp_dir / "nonexistent.txt")
|
|
assert is_valid is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_audio_quality(self, media_service, temp_dir):
|
|
"""Test audio quality checking."""
|
|
# Mock FFprobe to return valid duration
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
mock_process = AsyncMock()
|
|
mock_process.communicate.return_value = (b"10.5", b"") # 10.5 seconds
|
|
mock_process.returncode = 0
|
|
mock_subprocess.return_value = mock_process
|
|
|
|
test_file = temp_dir / "test.wav"
|
|
test_file.write_text("fake audio content")
|
|
|
|
is_valid = await media_service.check_audio_quality(test_file)
|
|
assert is_valid is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_audio_quality_too_short(self, media_service, temp_dir):
|
|
"""Test audio quality checking for files that are too short."""
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
mock_process = AsyncMock()
|
|
mock_process.communicate.return_value = (b"0.05", b"") # 0.05 seconds (too short)
|
|
mock_process.returncode = 0
|
|
mock_subprocess.return_value = mock_process
|
|
|
|
test_file = temp_dir / "test.wav"
|
|
test_file.write_text("fake audio content")
|
|
|
|
is_valid = await media_service.check_audio_quality(test_file)
|
|
assert is_valid is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_media_info(self, media_service, temp_dir):
|
|
"""Test getting media information."""
|
|
# Mock FFprobe to return JSON info
|
|
mock_json_response = {
|
|
"format": {
|
|
"duration": "120.5",
|
|
"format_name": "mp3",
|
|
"bit_rate": "128000",
|
|
"size": "1920000"
|
|
},
|
|
"streams": [
|
|
{
|
|
"codec_type": "audio",
|
|
"codec_name": "mp3",
|
|
"sample_rate": "44100",
|
|
"channels": 2
|
|
}
|
|
]
|
|
}
|
|
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
mock_process = AsyncMock()
|
|
mock_process.communicate.return_value = (
|
|
json.dumps(mock_json_response).encode(),
|
|
b""
|
|
)
|
|
mock_process.returncode = 0
|
|
mock_subprocess.return_value = mock_process
|
|
|
|
test_file = temp_dir / "test.mp3"
|
|
test_file.write_text("fake audio content")
|
|
|
|
info = await media_service.get_media_info(test_file)
|
|
assert "duration" in info
|
|
assert "mime_type" in info
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_preprocess_audio(self, media_service, temp_dir):
|
|
"""Test audio preprocessing."""
|
|
input_file = temp_dir / "input.mp3"
|
|
output_file = temp_dir / "output.wav"
|
|
input_file.write_text("fake input audio")
|
|
|
|
# Mock FFmpeg and FFprobe
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
# Mock FFmpeg conversion
|
|
mock_ffmpeg = AsyncMock()
|
|
mock_ffmpeg.communicate.return_value = (b"", b"")
|
|
mock_ffmpeg.returncode = 0
|
|
|
|
# Mock FFprobe for quality check
|
|
mock_ffprobe = AsyncMock()
|
|
mock_ffprobe.communicate.return_value = (b"10.5", b"") # Valid duration
|
|
mock_ffprobe.returncode = 0
|
|
|
|
mock_subprocess.side_effect = [mock_ffmpeg, mock_ffprobe]
|
|
|
|
success = await media_service.preprocess_audio(input_file, output_file)
|
|
assert success is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_preprocess_audio_ffmpeg_error(self, media_service, temp_dir):
|
|
"""Test audio preprocessing with FFmpeg error."""
|
|
input_file = temp_dir / "input.mp3"
|
|
output_file = temp_dir / "output.wav"
|
|
input_file.write_text("fake input audio")
|
|
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
mock_process = AsyncMock()
|
|
mock_process.communicate.return_value = (b"", b"FFmpeg error")
|
|
mock_process.returncode = 1
|
|
mock_subprocess.return_value = mock_process
|
|
|
|
success = await media_service.preprocess_audio(input_file, output_file)
|
|
assert success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_media_mock(self, media_service, temp_dir):
|
|
"""Test media download with mocked yt-dlp."""
|
|
test_url = "https://www.youtube.com/watch?v=test123"
|
|
|
|
# Mock yt-dlp
|
|
mock_info = {
|
|
"title": "Test Video",
|
|
"filesize": 1024 * 1024, # 1MB
|
|
"duration": 120,
|
|
"format": "mp4"
|
|
}
|
|
|
|
with patch("yt_dlp.YoutubeDL") as mock_ydl_class:
|
|
mock_ydl = MagicMock()
|
|
mock_ydl.extract_info.return_value = mock_info
|
|
mock_ydl_class.return_value.__enter__.return_value = mock_ydl
|
|
|
|
# Create a fake downloaded file
|
|
downloaded_file = temp_dir / "Test Video.mp4"
|
|
downloaded_file.write_text("fake video content")
|
|
|
|
# Mock file operations
|
|
with patch.object(media_service, "get_media_info") as mock_get_info:
|
|
mock_get_info.return_value = {
|
|
"duration": 120.0,
|
|
"mime_type": "mp4"
|
|
}
|
|
|
|
with patch.object(media_service, "_calculate_file_hash") as mock_hash:
|
|
mock_hash.return_value = "test_hash_123"
|
|
|
|
result = await media_service.download_media(test_url, temp_dir)
|
|
|
|
assert result.filename == "Test Video.mp4"
|
|
assert result.source_path == test_url
|
|
assert result.file_hash == "test_hash_123"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_media_file_too_large(self, media_service, temp_dir):
|
|
"""Test media download with file size validation."""
|
|
test_url = "https://www.youtube.com/watch?v=test123"
|
|
|
|
# Mock yt-dlp with large file
|
|
mock_info = {
|
|
"title": "Test Video",
|
|
"filesize": 600 * 1024 * 1024, # 600MB (exceeds 500MB limit)
|
|
"duration": 120,
|
|
"format": "mp4"
|
|
}
|
|
|
|
with patch("yt_dlp.YoutubeDL") as mock_ydl_class:
|
|
mock_ydl = MagicMock()
|
|
mock_ydl.extract_info.return_value = mock_info
|
|
mock_ydl_class.return_value.__enter__.return_value = mock_ydl
|
|
|
|
# The retry logic will catch the ValueError and retry, but since we're mocking
|
|
# the same response each time, it will eventually fail with RetryError
|
|
with pytest.raises(RetryError):
|
|
await media_service.download_media(test_url, temp_dir)
|
|
|
|
def test_create_media_service(self):
|
|
"""Test factory function for creating media service."""
|
|
service = create_media_service()
|
|
assert isinstance(service, MediaService)
|
|
assert service.name == "media_service"
|
|
|
|
# Test with custom config
|
|
custom_config = {"max_file_size_mb": 1000, "retry_attempts": 5}
|
|
service = create_media_service(custom_config)
|
|
assert service.max_file_size_mb == 1000
|
|
assert service.retry_attempts == 5
|
|
|
|
def test_media_service_protocol(self):
|
|
"""Test that MediaService implements MediaServiceProtocol."""
|
|
service = MediaService()
|
|
assert isinstance(service, MediaServiceProtocol)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_progress_hook(self, media_service):
|
|
"""Test progress hook functionality."""
|
|
# Test downloading progress
|
|
progress_data = {
|
|
"status": "downloading",
|
|
"downloaded_bytes": 1024,
|
|
"total_bytes": 2048,
|
|
"speed": 1000,
|
|
"eta": 1
|
|
}
|
|
|
|
# This should not raise any exceptions
|
|
media_service._progress_hook(progress_data)
|
|
|
|
# Test finished status
|
|
finished_data = {"status": "finished"}
|
|
media_service._progress_hook(finished_data)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_calculate_file_hash(self, media_service, temp_dir):
|
|
"""Test file hash calculation."""
|
|
test_file = temp_dir / "test.txt"
|
|
test_content = "test content for hashing"
|
|
test_file.write_text(test_content)
|
|
|
|
file_hash = await media_service._calculate_file_hash(test_file)
|
|
|
|
# Should be a valid SHA-256 hash (64 characters)
|
|
assert len(file_hash) == 64
|
|
assert all(c in "0123456789abcdef" for c in file_hash)
|