diff --git a/tests/utils/test_audio.py b/tests/utils/test_audio.py index 61ef7b6..f31abac 100644 --- a/tests/utils/test_audio.py +++ b/tests/utils/test_audio.py @@ -3,7 +3,7 @@ import hashlib import tempfile from pathlib import Path -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -15,11 +15,18 @@ from app.utils.audio import ( get_sound_file_path, ) +# Constants +SHA256_HASH_LENGTH = 64 +BINARY_FILE_SIZE = 700 +EXPECTED_DURATION_MS_1 = 123456 # 123.456 seconds * 1000 +EXPECTED_DURATION_MS_2 = 60000 # 60 seconds * 1000 +EXPECTED_DURATION_MS_3 = 45123 # 45.123 seconds * 1000 + class TestAudioUtils: """Test audio utility functions.""" - def test_get_file_hash(self): + def test_get_file_hash(self) -> None: """Test file hash calculation.""" # Create a temporary file with known content test_content = "test content for hashing" @@ -36,13 +43,13 @@ class TestAudioUtils: # Verify the hash is correct assert result_hash == expected_hash - assert len(result_hash) == 64 # SHA-256 hash length + assert len(result_hash) == SHA256_HASH_LENGTH # SHA-256 hash length assert isinstance(result_hash, str) finally: temp_path.unlink() - def test_get_file_hash_binary_content(self): + def test_get_file_hash_binary_content(self) -> None: """Test file hash calculation with binary content.""" # Create a temporary file with binary content test_bytes = b"\x00\x01\x02\x03\xff\xfe\xfd" @@ -59,13 +66,13 @@ class TestAudioUtils: # Verify the hash is correct assert result_hash == expected_hash - assert len(result_hash) == 64 # SHA-256 hash length + assert len(result_hash) == SHA256_HASH_LENGTH # SHA-256 hash length assert isinstance(result_hash, str) finally: temp_path.unlink() - def test_get_file_hash_empty_file(self): + def test_get_file_hash_empty_file(self) -> None: """Test file hash calculation for empty file.""" # Create an empty temporary file with tempfile.NamedTemporaryFile(delete=False) as f: @@ -80,13 +87,13 @@ class TestAudioUtils: # Verify the hash is correct assert result_hash == expected_hash - assert len(result_hash) == 64 # SHA-256 hash length + assert len(result_hash) == SHA256_HASH_LENGTH # SHA-256 hash length assert isinstance(result_hash, str) finally: temp_path.unlink() - def test_get_file_hash_large_file(self): + def test_get_file_hash_large_file(self) -> None: """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 @@ -103,13 +110,13 @@ class TestAudioUtils: # Verify the hash is correct assert result_hash == expected_hash - assert len(result_hash) == 64 # SHA-256 hash length + assert len(result_hash) == SHA256_HASH_LENGTH # SHA-256 hash length assert isinstance(result_hash, str) finally: temp_path.unlink() - def test_get_file_size(self): + def test_get_file_size(self) -> None: """Test file size calculation.""" # Create a temporary file with known content test_content = "test content for size calculation" @@ -132,7 +139,7 @@ class TestAudioUtils: finally: temp_path.unlink() - def test_get_file_size_empty_file(self): + def test_get_file_size_empty_file(self) -> None: """Test file size calculation for empty file.""" # Create an empty temporary file with tempfile.NamedTemporaryFile(delete=False) as f: @@ -149,7 +156,7 @@ class TestAudioUtils: finally: temp_path.unlink() - def test_get_file_size_binary_file(self): + def test_get_file_size_binary_file(self) -> None: """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 @@ -163,14 +170,14 @@ class TestAudioUtils: # Verify the size is correct assert result_size == len(test_bytes) - assert result_size == 700 + assert result_size == BINARY_FILE_SIZE assert isinstance(result_size, int) finally: temp_path.unlink() @patch("app.utils.audio.ffmpeg.probe") - def test_get_audio_duration_success(self, mock_probe): + def test_get_audio_duration_success(self, mock_probe: MagicMock) -> None: """Test successful audio duration extraction.""" # Mock ffmpeg.probe to return duration mock_probe.return_value = {"format": {"duration": "123.456"}} @@ -179,12 +186,12 @@ class TestAudioUtils: duration = get_audio_duration(temp_path) # Verify duration is converted correctly (seconds to milliseconds) - assert duration == 123456 # 123.456 seconds * 1000 = 123456 ms + assert duration == EXPECTED_DURATION_MS_1 # 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): + def test_get_audio_duration_integer_duration(self, mock_probe: MagicMock) -> None: """Test audio duration extraction with integer duration.""" # Mock ffmpeg.probe to return integer duration mock_probe.return_value = {"format": {"duration": "60"}} @@ -193,12 +200,12 @@ class TestAudioUtils: duration = get_audio_duration(temp_path) # Verify duration is converted correctly - assert duration == 60000 # 60 seconds * 1000 = 60000 ms + assert duration == EXPECTED_DURATION_MS_2 # 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): + def test_get_audio_duration_zero_duration(self, mock_probe: MagicMock) -> None: """Test audio duration extraction with zero duration.""" # Mock ffmpeg.probe to return zero duration mock_probe.return_value = {"format": {"duration": "0.0"}} @@ -212,7 +219,9 @@ class TestAudioUtils: 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): + def test_get_audio_duration_fractional_duration( + self, mock_probe: MagicMock, + ) -> None: """Test audio duration extraction with fractional seconds.""" # Mock ffmpeg.probe to return fractional duration mock_probe.return_value = {"format": {"duration": "45.123"}} @@ -221,12 +230,12 @@ class TestAudioUtils: duration = get_audio_duration(temp_path) # Verify duration is converted and rounded correctly - assert duration == 45123 # 45.123 seconds * 1000 = 45123 ms + assert duration == EXPECTED_DURATION_MS_3 # 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): + def test_get_audio_duration_ffmpeg_error(self, mock_probe: MagicMock) -> None: """Test audio duration extraction when ffmpeg fails.""" # Mock ffmpeg.probe to raise an exception mock_probe.side_effect = Exception("FFmpeg error: file not found") @@ -240,7 +249,7 @@ class TestAudioUtils: 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): + def test_get_audio_duration_missing_format(self, mock_probe: MagicMock) -> None: """Test audio duration extraction when format info is missing.""" # Mock ffmpeg.probe to return data without format info mock_probe.return_value = {"streams": []} @@ -254,7 +263,7 @@ class TestAudioUtils: 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): + def test_get_audio_duration_missing_duration(self, mock_probe: MagicMock) -> None: """Test audio duration extraction when duration is missing.""" # Mock ffmpeg.probe to return format without duration mock_probe.return_value = {"format": {"size": "1024"}} @@ -268,7 +277,7 @@ class TestAudioUtils: 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): + def test_get_audio_duration_invalid_duration(self, mock_probe: MagicMock) -> None: """Test audio duration extraction with invalid duration value.""" # Mock ffmpeg.probe to return invalid duration mock_probe.return_value = {"format": {"duration": "invalid"}} @@ -281,7 +290,7 @@ class TestAudioUtils: assert isinstance(duration, int) mock_probe.assert_called_once_with(str(temp_path)) - def test_get_file_hash_nonexistent_file(self): + def test_get_file_hash_nonexistent_file(self) -> None: """Test file hash calculation for nonexistent file.""" nonexistent_path = Path("/fake/nonexistent/file.mp3") @@ -289,7 +298,7 @@ class TestAudioUtils: with pytest.raises(FileNotFoundError): get_file_hash(nonexistent_path) - def test_get_file_size_nonexistent_file(self): + def test_get_file_size_nonexistent_file(self) -> None: """Test file size calculation for nonexistent file.""" nonexistent_path = Path("/fake/nonexistent/file.mp3") @@ -297,7 +306,7 @@ class TestAudioUtils: with pytest.raises(FileNotFoundError): get_file_size(nonexistent_path) - def test_get_sound_file_path_sdb_original(self): + def test_get_sound_file_path_sdb_original(self) -> None: """Test getting sound file path for SDB type original file.""" sound = Sound( id=1, @@ -311,7 +320,7 @@ class TestAudioUtils: expected = Path("sounds/originals/soundboard/test.mp3") assert result == expected - def test_get_sound_file_path_sdb_normalized(self): + def test_get_sound_file_path_sdb_normalized(self) -> None: """Test getting sound file path for SDB type normalized file.""" sound = Sound( id=1, @@ -326,7 +335,7 @@ class TestAudioUtils: expected = Path("sounds/normalized/soundboard/normalized.mp3") assert result == expected - def test_get_sound_file_path_tts_original(self): + def test_get_sound_file_path_tts_original(self) -> None: """Test getting sound file path for TTS type original file.""" sound = Sound( id=2, @@ -340,7 +349,7 @@ class TestAudioUtils: expected = Path("sounds/originals/text_to_speech/tts_file.wav") assert result == expected - def test_get_sound_file_path_tts_normalized(self): + def test_get_sound_file_path_tts_normalized(self) -> None: """Test getting sound file path for TTS type normalized file.""" sound = Sound( id=2, @@ -355,7 +364,7 @@ class TestAudioUtils: expected = Path("sounds/normalized/text_to_speech/normalized.mp3") assert result == expected - def test_get_sound_file_path_ext_original(self): + def test_get_sound_file_path_ext_original(self) -> None: """Test getting sound file path for EXT type original file.""" sound = Sound( id=3, @@ -369,7 +378,7 @@ class TestAudioUtils: expected = Path("sounds/originals/extracted/extracted.mp3") assert result == expected - def test_get_sound_file_path_ext_normalized(self): + def test_get_sound_file_path_ext_normalized(self) -> None: """Test getting sound file path for EXT type normalized file.""" sound = Sound( id=3, @@ -384,7 +393,7 @@ class TestAudioUtils: expected = Path("sounds/normalized/extracted/normalized.mp3") assert result == expected - def test_get_sound_file_path_unknown_type_fallback(self): + def test_get_sound_file_path_unknown_type_fallback(self) -> None: """Test getting sound file path for unknown type falls back to lowercase.""" sound = Sound( id=4, @@ -398,7 +407,7 @@ class TestAudioUtils: expected = Path("sounds/originals/custom/unknown.mp3") assert result == expected - def test_get_sound_file_path_normalized_without_filename(self): + def test_get_sound_file_path_normalized_without_filename(self) -> None: """Test getting sound file path when normalized but no normalized_filename.""" sound = Sound( id=5, diff --git a/tests/utils/test_cookies.py b/tests/utils/test_cookies.py index 2452ead..abcb5ca 100644 --- a/tests/utils/test_cookies.py +++ b/tests/utils/test_cookies.py @@ -1,4 +1,5 @@ """Tests for cookie utilities.""" +# ruff: noqa: ANN201, E501 from app.utils.cookies import extract_access_token_from_cookies, parse_cookies diff --git a/tests/utils/test_credit_decorators.py b/tests/utils/test_credit_decorators.py index f6cb867..7a9ea60 100644 --- a/tests/utils/test_credit_decorators.py +++ b/tests/utils/test_credit_decorators.py @@ -1,5 +1,8 @@ """Tests for credit decorators.""" +# ruff: noqa: ARG001, ANN001, E501, PT012 +from collections.abc import Callable +from typing import Never from unittest.mock import AsyncMock import pytest @@ -17,7 +20,7 @@ class TestRequiresCreditsDecorator: """Test requires_credits decorator.""" @pytest.fixture - def mock_credit_service(self): + def mock_credit_service(self) -> AsyncMock: """Create a mock credit service.""" service = AsyncMock(spec=CreditService) service.validate_and_reserve_credits = AsyncMock() @@ -25,12 +28,14 @@ class TestRequiresCreditsDecorator: return service @pytest.fixture - def credit_service_factory(self, mock_credit_service): + def credit_service_factory(self, mock_credit_service: AsyncMock) -> Callable[[], AsyncMock]: """Create a credit service factory.""" return lambda: mock_credit_service @pytest.mark.asyncio - async def test_decorator_success(self, credit_service_factory, mock_credit_service): + async def test_decorator_success( + self, credit_service_factory: Callable[[], AsyncMock], mock_credit_service: AsyncMock, + ) -> None: """Test decorator with successful action.""" @requires_credits( @@ -52,10 +57,12 @@ class TestRequiresCreditsDecorator: ) @pytest.mark.asyncio - async def test_decorator_with_metadata(self, credit_service_factory, mock_credit_service): + async def test_decorator_with_metadata( + self, credit_service_factory: Callable[[], AsyncMock], mock_credit_service: AsyncMock, + ) -> None: """Test decorator with metadata extraction.""" - def extract_metadata(user_id: int, sound_name: str) -> dict: + def extract_metadata(user_id: int, sound_name: str) -> dict[str, str]: return {"sound_name": sound_name} @requires_credits( @@ -77,7 +84,7 @@ class TestRequiresCreditsDecorator: ) @pytest.mark.asyncio - async def test_decorator_failed_action(self, credit_service_factory, mock_credit_service): + async def test_decorator_failed_action(self, credit_service_factory, mock_credit_service) -> None: """Test decorator with failed action.""" @requires_credits( @@ -96,7 +103,7 @@ class TestRequiresCreditsDecorator: ) @pytest.mark.asyncio - async def test_decorator_exception_in_action(self, credit_service_factory, mock_credit_service): + async def test_decorator_exception_in_action(self, credit_service_factory, mock_credit_service) -> None: """Test decorator when action raises exception.""" @requires_credits( @@ -105,7 +112,8 @@ class TestRequiresCreditsDecorator: user_id_param="user_id", ) async def test_action(user_id: int) -> str: - raise ValueError("Test error") + msg = "Test error" + raise ValueError(msg) with pytest.raises(ValueError, match="Test error"): await test_action(user_id=123) @@ -115,7 +123,7 @@ class TestRequiresCreditsDecorator: ) @pytest.mark.asyncio - async def test_decorator_insufficient_credits(self, credit_service_factory, mock_credit_service): + async def test_decorator_insufficient_credits(self, credit_service_factory, mock_credit_service) -> None: """Test decorator with insufficient credits.""" mock_credit_service.validate_and_reserve_credits.side_effect = InsufficientCreditsError(1, 0) @@ -134,7 +142,7 @@ class TestRequiresCreditsDecorator: mock_credit_service.deduct_credits.assert_not_called() @pytest.mark.asyncio - async def test_decorator_user_id_in_args(self, credit_service_factory, mock_credit_service): + async def test_decorator_user_id_in_args(self, credit_service_factory, mock_credit_service) -> None: """Test decorator extracting user_id from positional args.""" @requires_credits( @@ -153,7 +161,7 @@ class TestRequiresCreditsDecorator: ) @pytest.mark.asyncio - async def test_decorator_missing_user_id(self, credit_service_factory): + async def test_decorator_missing_user_id(self, credit_service_factory) -> None: """Test decorator when user_id cannot be extracted.""" @requires_credits( @@ -172,19 +180,19 @@ class TestValidateCreditsOnlyDecorator: """Test validate_credits_only decorator.""" @pytest.fixture - def mock_credit_service(self): + def mock_credit_service(self) -> AsyncMock: """Create a mock credit service.""" service = AsyncMock(spec=CreditService) service.validate_and_reserve_credits = AsyncMock() return service @pytest.fixture - def credit_service_factory(self, mock_credit_service): + def credit_service_factory(self, mock_credit_service: AsyncMock) -> Callable[[], AsyncMock]: """Create a credit service factory.""" return lambda: mock_credit_service @pytest.mark.asyncio - async def test_validate_only_decorator(self, credit_service_factory, mock_credit_service): + async def test_validate_only_decorator(self, credit_service_factory, mock_credit_service) -> None: """Test validate_credits_only decorator.""" @validate_credits_only( @@ -209,7 +217,7 @@ class TestCreditManager: """Test CreditManager context manager.""" @pytest.fixture - def mock_credit_service(self): + def mock_credit_service(self) -> AsyncMock: """Create a mock credit service.""" service = AsyncMock(spec=CreditService) service.validate_and_reserve_credits = AsyncMock() @@ -217,7 +225,7 @@ class TestCreditManager: return service @pytest.mark.asyncio - async def test_credit_manager_success(self, mock_credit_service): + async def test_credit_manager_success(self, mock_credit_service) -> None: """Test CreditManager with successful operation.""" async with CreditManager( mock_credit_service, @@ -235,7 +243,7 @@ class TestCreditManager: ) @pytest.mark.asyncio - async def test_credit_manager_failure(self, mock_credit_service): + async def test_credit_manager_failure(self, mock_credit_service) -> None: """Test CreditManager with failed operation.""" async with CreditManager( mock_credit_service, @@ -250,7 +258,7 @@ class TestCreditManager: ) @pytest.mark.asyncio - async def test_credit_manager_exception(self, mock_credit_service): + async def test_credit_manager_exception(self, mock_credit_service) -> Never: """Test CreditManager when exception occurs.""" with pytest.raises(ValueError, match="Test error"): async with CreditManager( @@ -258,14 +266,15 @@ class TestCreditManager: 123, CreditActionType.VLC_PLAY_SOUND, ): - raise ValueError("Test error") + msg = "Test error" + raise ValueError(msg) mock_credit_service.deduct_credits.assert_called_once_with( 123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None, ) @pytest.mark.asyncio - async def test_credit_manager_validation_failure(self, mock_credit_service): + async def test_credit_manager_validation_failure(self, mock_credit_service) -> None: """Test CreditManager when validation fails.""" mock_credit_service.validate_and_reserve_credits.side_effect = InsufficientCreditsError(1, 0)