"""Tests for audio utility functions.""" import hashlib import tempfile from pathlib import Path from unittest.mock import patch import pytest from app.models.sound import Sound from app.utils.audio import ( get_audio_duration, get_file_hash, get_file_size, get_sound_file_path, ) class TestAudioUtils: """Test audio utility functions.""" def test_get_file_hash(self): """Test file hash calculation.""" # Create a temporary file with known content test_content = "test content for hashing" with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: f.write(test_content) temp_path = Path(f.name) try: # Calculate hash using our function result_hash = get_file_hash(temp_path) # Calculate expected hash manually expected_hash = hashlib.sha256(test_content.encode()).hexdigest() # Verify the hash is correct assert result_hash == expected_hash assert len(result_hash) == 64 # SHA-256 hash length assert isinstance(result_hash, str) finally: temp_path.unlink() def test_get_file_hash_binary_content(self): """Test file hash calculation with binary content.""" # Create a temporary file with binary content test_bytes = b"\x00\x01\x02\x03\xff\xfe\xfd" with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: f.write(test_bytes) temp_path = Path(f.name) try: # Calculate hash using our function result_hash = get_file_hash(temp_path) # Calculate expected hash manually expected_hash = hashlib.sha256(test_bytes).hexdigest() # Verify the hash is correct assert result_hash == expected_hash assert len(result_hash) == 64 # SHA-256 hash length assert isinstance(result_hash, str) finally: temp_path.unlink() def test_get_file_hash_empty_file(self): """Test file hash calculation for empty file.""" # Create an empty temporary file with tempfile.NamedTemporaryFile(delete=False) as f: temp_path = Path(f.name) try: # Calculate hash using our function result_hash = get_file_hash(temp_path) # Calculate expected hash for empty content expected_hash = hashlib.sha256(b"").hexdigest() # Verify the hash is correct assert result_hash == expected_hash assert len(result_hash) == 64 # SHA-256 hash length assert isinstance(result_hash, str) finally: temp_path.unlink() def test_get_file_hash_large_file(self): """Test file hash calculation for large file (tests chunked reading).""" # Create a large temporary file (larger than 4096 bytes chunk size) test_content = "A" * 10000 # 10KB of 'A' characters with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: f.write(test_content) temp_path = Path(f.name) try: # Calculate hash using our function result_hash = get_file_hash(temp_path) # Calculate expected hash manually expected_hash = hashlib.sha256(test_content.encode()).hexdigest() # Verify the hash is correct assert result_hash == expected_hash assert len(result_hash) == 64 # SHA-256 hash length assert isinstance(result_hash, str) finally: temp_path.unlink() def test_get_file_size(self): """Test file size calculation.""" # Create a temporary file with known content test_content = "test content for size calculation" with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: f.write(test_content) temp_path = Path(f.name) try: # Get size using our function result_size = get_file_size(temp_path) # Get expected size using pathlib directly expected_size = temp_path.stat().st_size # Verify the size is correct assert result_size == expected_size assert result_size > 0 assert isinstance(result_size, int) finally: temp_path.unlink() def test_get_file_size_empty_file(self): """Test file size calculation for empty file.""" # Create an empty temporary file with tempfile.NamedTemporaryFile(delete=False) as f: temp_path = Path(f.name) try: # Get size using our function result_size = get_file_size(temp_path) # Verify the size is zero assert result_size == 0 assert isinstance(result_size, int) finally: temp_path.unlink() def test_get_file_size_binary_file(self): """Test file size calculation for binary file.""" # Create a temporary file with binary content test_bytes = b"\x00\x01\x02\x03\xff\xfe\xfd" * 100 # 700 bytes with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: f.write(test_bytes) temp_path = Path(f.name) try: # Get size using our function result_size = get_file_size(temp_path) # Verify the size is correct assert result_size == len(test_bytes) assert result_size == 700 assert isinstance(result_size, int) finally: temp_path.unlink() @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_success(self, mock_probe): """Test successful audio duration extraction.""" # Mock ffmpeg.probe to return duration mock_probe.return_value = {"format": {"duration": "123.456"}} temp_path = Path("/fake/path/test.mp3") duration = get_audio_duration(temp_path) # Verify duration is converted correctly (seconds to milliseconds) assert duration == 123456 # 123.456 seconds * 1000 = 123456 ms assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_integer_duration(self, mock_probe): """Test audio duration extraction with integer duration.""" # Mock ffmpeg.probe to return integer duration mock_probe.return_value = {"format": {"duration": "60"}} temp_path = Path("/fake/path/test.wav") duration = get_audio_duration(temp_path) # Verify duration is converted correctly assert duration == 60000 # 60 seconds * 1000 = 60000 ms assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_zero_duration(self, mock_probe): """Test audio duration extraction with zero duration.""" # Mock ffmpeg.probe to return zero duration mock_probe.return_value = {"format": {"duration": "0.0"}} temp_path = Path("/fake/path/silent.mp3") duration = get_audio_duration(temp_path) # Verify duration is zero assert duration == 0 assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_fractional_duration(self, mock_probe): """Test audio duration extraction with fractional seconds.""" # Mock ffmpeg.probe to return fractional duration mock_probe.return_value = {"format": {"duration": "45.123"}} temp_path = Path("/fake/path/test.flac") duration = get_audio_duration(temp_path) # Verify duration is converted and rounded correctly assert duration == 45123 # 45.123 seconds * 1000 = 45123 ms assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_ffmpeg_error(self, mock_probe): """Test audio duration extraction when ffmpeg fails.""" # Mock ffmpeg.probe to raise an exception mock_probe.side_effect = Exception("FFmpeg error: file not found") temp_path = Path("/fake/path/nonexistent.mp3") duration = get_audio_duration(temp_path) # Verify duration defaults to 0 on error assert duration == 0 assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_missing_format(self, mock_probe): """Test audio duration extraction when format info is missing.""" # Mock ffmpeg.probe to return data without format info mock_probe.return_value = {"streams": []} temp_path = Path("/fake/path/corrupt.mp3") duration = get_audio_duration(temp_path) # Verify duration defaults to 0 when format info is missing assert duration == 0 assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_missing_duration(self, mock_probe): """Test audio duration extraction when duration is missing.""" # Mock ffmpeg.probe to return format without duration mock_probe.return_value = {"format": {"size": "1024"}} temp_path = Path("/fake/path/noduration.mp3") duration = get_audio_duration(temp_path) # Verify duration defaults to 0 when duration is missing assert duration == 0 assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_invalid_duration(self, mock_probe): """Test audio duration extraction with invalid duration value.""" # Mock ffmpeg.probe to return invalid duration mock_probe.return_value = {"format": {"duration": "invalid"}} temp_path = Path("/fake/path/invalid.mp3") duration = get_audio_duration(temp_path) # Verify duration defaults to 0 when duration is invalid assert duration == 0 assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) def test_get_file_hash_nonexistent_file(self): """Test file hash calculation for nonexistent file.""" nonexistent_path = Path("/fake/nonexistent/file.mp3") # Should raise FileNotFoundError for nonexistent file with pytest.raises(FileNotFoundError): get_file_hash(nonexistent_path) def test_get_file_size_nonexistent_file(self): """Test file size calculation for nonexistent file.""" nonexistent_path = Path("/fake/nonexistent/file.mp3") # Should raise FileNotFoundError for nonexistent file with pytest.raises(FileNotFoundError): get_file_size(nonexistent_path) def test_get_sound_file_path_sdb_original(self): """Test getting sound file path for SDB type original file.""" sound = Sound( id=1, name="Test Sound", filename="test.mp3", type="SDB", is_normalized=False, ) result = get_sound_file_path(sound) expected = Path("sounds/originals/soundboard/test.mp3") assert result == expected def test_get_sound_file_path_sdb_normalized(self): """Test getting sound file path for SDB type normalized file.""" sound = Sound( id=1, name="Test Sound", filename="original.mp3", normalized_filename="normalized.mp3", type="SDB", is_normalized=True, ) result = get_sound_file_path(sound) expected = Path("sounds/normalized/soundboard/normalized.mp3") assert result == expected def test_get_sound_file_path_tts_original(self): """Test getting sound file path for TTS type original file.""" sound = Sound( id=2, name="TTS Sound", filename="tts_file.wav", type="TTS", is_normalized=False, ) result = get_sound_file_path(sound) expected = Path("sounds/originals/text_to_speech/tts_file.wav") assert result == expected def test_get_sound_file_path_tts_normalized(self): """Test getting sound file path for TTS type normalized file.""" sound = Sound( id=2, name="TTS Sound", filename="original.wav", normalized_filename="normalized.mp3", type="TTS", is_normalized=True, ) result = get_sound_file_path(sound) expected = Path("sounds/normalized/text_to_speech/normalized.mp3") assert result == expected def test_get_sound_file_path_ext_original(self): """Test getting sound file path for EXT type original file.""" sound = Sound( id=3, name="Extracted Sound", filename="extracted.mp3", type="EXT", is_normalized=False, ) result = get_sound_file_path(sound) expected = Path("sounds/originals/extracted/extracted.mp3") assert result == expected def test_get_sound_file_path_ext_normalized(self): """Test getting sound file path for EXT type normalized file.""" sound = Sound( id=3, name="Extracted Sound", filename="original.mp3", normalized_filename="normalized.mp3", type="EXT", is_normalized=True, ) result = get_sound_file_path(sound) expected = Path("sounds/normalized/extracted/normalized.mp3") assert result == expected def test_get_sound_file_path_unknown_type_fallback(self): """Test getting sound file path for unknown type falls back to lowercase.""" sound = Sound( id=4, name="Unknown Type Sound", filename="unknown.mp3", type="CUSTOM", is_normalized=False, ) result = get_sound_file_path(sound) expected = Path("sounds/originals/custom/unknown.mp3") assert result == expected def test_get_sound_file_path_normalized_without_filename(self): """Test getting sound file path when normalized but no normalized_filename.""" sound = Sound( id=5, name="Test Sound", filename="original.mp3", normalized_filename=None, type="SDB", is_normalized=True, # True but no normalized_filename ) result = get_sound_file_path(sound) # Should fall back to original file expected = Path("sounds/originals/soundboard/original.mp3") assert result == expected