trax/tests/test_media_service.py

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)