"""Tests for sound normalizer API endpoints.""" from unittest.mock import patch import pytest from httpx import ASGITransport, AsyncClient from app.models.user import User from app.services.sound_normalizer import NormalizationResults class TestSoundNormalizerEndpoints: """Test sound normalizer API endpoints.""" @pytest.mark.asyncio async def test_normalize_all_sounds_success( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test successful normalization of all sounds.""" mock_results: NormalizationResults = { "processed": 3, "normalized": 2, "skipped": 1, "errors": 0, "files": [ { "filename": "test1.mp3", "status": "normalized", "reason": None, "original_path": "/fake/test1.mp3", "normalized_path": "/fake/test1_normalized.mp3", "normalized_filename": "test1_normalized.mp3", "normalized_duration": 5000, "normalized_size": 1024, "normalized_hash": "norm_hash1", "id": 1, "error": None, }, { "filename": "test2.wav", "status": "normalized", "reason": None, "original_path": "/fake/test2.wav", "normalized_path": "/fake/test2_normalized.mp3", "normalized_filename": "test2_normalized.mp3", "normalized_duration": 7000, "normalized_size": 2048, "normalized_hash": "norm_hash2", "id": 2, "error": None, }, { "filename": "test3.mp3", "status": "skipped", "reason": "already normalized", "original_path": None, "normalized_path": None, "normalized_filename": None, "normalized_duration": None, "normalized_size": None, "normalized_hash": None, "id": 3, "error": None, }, ], } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_all_sounds" ) as mock_normalize: mock_normalize.return_value = mock_results response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/all" ) assert response.status_code == 200 data = response.json() assert "message" in data assert "Sound normalization completed" in data["message"] assert "results" in data results = data["results"] assert results["processed"] == 3 assert results["normalized"] == 2 assert results["skipped"] == 1 assert results["errors"] == 0 assert len(results["files"]) == 3 @pytest.mark.asyncio async def test_normalize_all_sounds_with_force( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test normalization with force parameter.""" mock_results: NormalizationResults = { "processed": 1, "normalized": 1, "skipped": 0, "errors": 0, "files": [], } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_all_sounds" ) as mock_normalize: mock_normalize.return_value = mock_results response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/all", params={"force": True} ) assert response.status_code == 200 # Verify force parameter was passed mock_normalize.assert_called_once_with(force=True, one_pass=None) @pytest.mark.asyncio async def test_normalize_all_sounds_with_one_pass( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test normalization with one_pass parameter.""" mock_results: NormalizationResults = { "processed": 1, "normalized": 1, "skipped": 0, "errors": 0, "files": [], } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_all_sounds" ) as mock_normalize: mock_normalize.return_value = mock_results response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/all", params={"one_pass": True} ) assert response.status_code == 200 # Verify one_pass parameter was passed mock_normalize.assert_called_once_with(force=False, one_pass=True) @pytest.mark.asyncio async def test_normalize_all_sounds_unauthenticated(self, client: AsyncClient): """Test normalizing sounds without authentication.""" response = await client.post("/api/v1/sounds/normalize/all") assert response.status_code == 401 data = response.json() assert "Could not validate credentials" in data["detail"] @pytest.mark.asyncio async def test_normalize_all_sounds_non_admin( self, test_app, test_user: User, ): """Test normalizing sounds with non-admin user.""" from app.core.dependencies import get_current_active_user_flexible # Override the dependency to return regular user async def override_get_current_user(): test_user.role = "user" return test_user test_app.dependency_overrides[get_current_active_user_flexible] = ( override_get_current_user ) headers = {"API-TOKEN": "test_api_token"} async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test", ) as client: response = await client.post( "/api/v1/sounds/normalize/all", headers=headers ) assert response.status_code == 403 data = response.json() assert "Only administrators can normalize sounds" in data["detail"] # Clean up override test_app.dependency_overrides.pop(get_current_active_user_flexible, None) @pytest.mark.asyncio async def test_normalize_all_sounds_service_error( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test normalization when service raises an error.""" with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_all_sounds" ) as mock_normalize: mock_normalize.side_effect = Exception("Normalization service failed") response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/all" ) assert response.status_code == 500 data = response.json() assert "Failed to normalize sounds" in data["detail"] assert "Normalization service failed" in data["detail"] @pytest.mark.asyncio async def test_normalize_sounds_by_type_success( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test successful normalization by sound type.""" mock_results: NormalizationResults = { "processed": 2, "normalized": 2, "skipped": 0, "errors": 0, "files": [ { "filename": "sdb1.mp3", "status": "normalized", "reason": None, "original_path": "/fake/sdb1.mp3", "normalized_path": "/fake/sdb1_normalized.mp3", "normalized_filename": "sdb1_normalized.mp3", "normalized_duration": 4000, "normalized_size": 800, "normalized_hash": "sdb_hash1", "id": 10, "error": None, }, { "filename": "sdb2.wav", "status": "normalized", "reason": None, "original_path": "/fake/sdb2.wav", "normalized_path": "/fake/sdb2_normalized.mp3", "normalized_filename": "sdb2_normalized.mp3", "normalized_duration": 6000, "normalized_size": 1200, "normalized_hash": "sdb_hash2", "id": 11, "error": None, }, ], } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_sounds_by_type" ) as mock_normalize: mock_normalize.return_value = mock_results response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/type/SDB" ) assert response.status_code == 200 data = response.json() assert "Normalization of SDB sounds completed" in data["message"] assert "results" in data results = data["results"] assert results["processed"] == 2 assert results["normalized"] == 2 assert len(results["files"]) == 2 # Verify the service was called with correct type mock_normalize.assert_called_once_with( sound_type="SDB", force=False, one_pass=None ) @pytest.mark.asyncio async def test_normalize_sounds_by_type_invalid_type( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test normalization with invalid sound type.""" response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/type/INVALID" ) assert response.status_code == 400 data = response.json() assert "Invalid sound type" in data["detail"] assert "Must be one of: SDB, TTS, EXT" in data["detail"] @pytest.mark.asyncio async def test_normalize_sounds_by_type_with_params( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test normalization by type with force and one_pass parameters.""" mock_results: NormalizationResults = { "processed": 1, "normalized": 1, "skipped": 0, "errors": 0, "files": [], } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_sounds_by_type" ) as mock_normalize: mock_normalize.return_value = mock_results response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/type/TTS", params={"force": True, "one_pass": False}, ) assert response.status_code == 200 # Verify parameters were passed correctly mock_normalize.assert_called_once_with( sound_type="TTS", force=True, one_pass=False ) @pytest.mark.asyncio async def test_normalize_sound_by_id_success( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test successful normalization of a specific sound.""" # Mock the sound mock_sound = type( "Sound", (), { "id": 42, "filename": "specific_sound.mp3", "type": "SDB", "name": "Specific Sound", }, )() # Mock normalization result mock_result = { "filename": "specific_sound.mp3", "status": "normalized", "reason": None, "original_path": "/fake/specific_sound.mp3", "normalized_path": "/fake/specific_sound_normalized.mp3", "normalized_filename": "specific_sound_normalized.mp3", "normalized_duration": 8000, "normalized_size": 1600, "normalized_hash": "specific_hash", "id": 42, "error": None, } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_sound" ) as mock_normalize_sound, patch( "app.repositories.sound.SoundRepository.get_by_id" ) as mock_get_sound: mock_get_sound.return_value = mock_sound mock_normalize_sound.return_value = mock_result response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/42" ) assert response.status_code == 200 data = response.json() assert "Sound normalization normalized" in data["message"] assert "specific_sound.mp3" in data["message"] assert data["status"] == "normalized" assert data["normalized_filename"] == "specific_sound_normalized.mp3" # Verify sound was retrieved and normalized mock_get_sound.assert_called_once_with(42) mock_normalize_sound.assert_called_once() @pytest.mark.asyncio async def test_normalize_sound_by_id_not_found( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test normalization of non-existent sound.""" with patch( "app.repositories.sound.SoundRepository.get_by_id" ) as mock_get_sound: mock_get_sound.return_value = None response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/999" ) assert response.status_code == 404 data = response.json() assert "Sound with ID 999 not found" in data["detail"] @pytest.mark.asyncio async def test_normalize_sound_by_id_normalization_error( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test normalization when the sound normalization fails.""" # Mock the sound mock_sound = type( "Sound", (), { "id": 42, "filename": "error_sound.mp3", "type": "SDB", "name": "Error Sound", }, )() # Mock normalization error result mock_result = { "filename": "error_sound.mp3", "status": "error", "reason": None, "original_path": "/fake/error_sound.mp3", "normalized_path": None, "normalized_filename": None, "normalized_duration": None, "normalized_size": None, "normalized_hash": None, "id": 42, "error": "File format not supported", } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_sound" ) as mock_normalize_sound, patch( "app.repositories.sound.SoundRepository.get_by_id" ) as mock_get_sound: mock_get_sound.return_value = mock_sound mock_normalize_sound.return_value = mock_result response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/42" ) assert response.status_code == 500 data = response.json() assert "Failed to normalize sound" in data["detail"] assert "File format not supported" in data["detail"] @pytest.mark.asyncio async def test_normalize_sound_by_id_with_params( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test sound normalization with force and one_pass parameters.""" # Mock the sound mock_sound = type( "Sound", (), { "id": 42, "filename": "param_sound.mp3", "type": "SDB", "name": "Param Sound", }, )() # Mock normalization result mock_result = { "filename": "param_sound.mp3", "status": "normalized", "reason": None, "original_path": "/fake/param_sound.mp3", "normalized_path": "/fake/param_sound_normalized.mp3", "normalized_filename": "param_sound_normalized.mp3", "normalized_duration": 5000, "normalized_size": 1000, "normalized_hash": "param_hash", "id": 42, "error": None, } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_sound" ) as mock_normalize_sound, patch( "app.repositories.sound.SoundRepository.get_by_id" ) as mock_get_sound: mock_get_sound.return_value = mock_sound mock_normalize_sound.return_value = mock_result response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/42", params={"force": True, "one_pass": True}, ) assert response.status_code == 200 # Verify parameters were passed to normalize_sound call_args = mock_normalize_sound.call_args assert call_args[1]["force"] == True assert call_args[1]["one_pass"] == True @pytest.mark.asyncio async def test_normalize_sound_by_id_skipped( self, authenticated_admin_client: AsyncClient, admin_user: User, ): """Test normalization when sound is already normalized and not forced.""" # Mock the sound mock_sound = type( "Sound", (), { "id": 42, "filename": "already_normalized.mp3", "type": "SDB", "name": "Already Normalized", }, )() # Mock skipped result mock_result = { "filename": "already_normalized.mp3", "status": "skipped", "reason": "already normalized", "original_path": None, "normalized_path": None, "normalized_filename": None, "normalized_duration": None, "normalized_size": None, "normalized_hash": None, "id": 42, "error": None, } with patch( "app.services.sound_normalizer.SoundNormalizerService.normalize_sound" ) as mock_normalize_sound, patch( "app.repositories.sound.SoundRepository.get_by_id" ) as mock_get_sound: mock_get_sound.return_value = mock_sound mock_normalize_sound.return_value = mock_result response = await authenticated_admin_client.post( "/api/v1/sounds/normalize/42" ) assert response.status_code == 200 data = response.json() assert "Sound normalization skipped" in data["message"] assert data["status"] == "skipped" assert data["reason"] == "already normalized" @pytest.mark.asyncio async def test_normalize_sound_by_id_unauthenticated(self, client: AsyncClient): """Test normalizing a specific sound without authentication.""" response = await client.post("/api/v1/sounds/normalize/42") assert response.status_code == 401 data = response.json() assert "Could not validate credentials" in data["detail"] @pytest.mark.asyncio async def test_normalize_sound_by_id_non_admin( self, test_app, test_user: User, ): """Test normalizing a specific sound with non-admin user.""" from app.core.dependencies import get_current_active_user_flexible # Override the dependency to return regular user async def override_get_current_user(): test_user.role = "user" return test_user test_app.dependency_overrides[get_current_active_user_flexible] = ( override_get_current_user ) headers = {"API-TOKEN": "test_api_token"} async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test", ) as client: response = await client.post( "/api/v1/sounds/normalize/42", headers=headers ) assert response.status_code == 403 data = response.json() assert "Only administrators can normalize sounds" in data["detail"] # Clean up override test_app.dependency_overrides.pop(get_current_active_user_flexible, None)