"""Tests for file serving API endpoints.""" import tempfile from pathlib import Path from unittest.mock import patch import pytest import pytest_asyncio from fastapi import status from httpx import AsyncClient from sqlmodel.ext.asyncio.session import AsyncSession from app.models.sound import Sound from app.models.user import User class TestFilesEndpoints: """Test file serving endpoints.""" @pytest_asyncio.fixture async def test_sound( self, test_session: AsyncSession, ) -> Sound: """Create a test sound.""" sound = Sound( type="SDB", name="Test Sound", filename="test.mp3", duration=30000, size=1024, hash="test_hash", play_count=0, is_normalized=False, is_music=True, is_deletable=True, ) test_session.add(sound) await test_session.commit() await test_session.refresh(sound) return sound @pytest_asyncio.fixture async def test_sound_with_thumbnail( self, test_session: AsyncSession, ) -> Sound: """Create a test sound with thumbnail.""" sound = Sound( type="EXT", name="Test Extracted Sound", filename="extracted.mp3", duration=60000, size=2048, hash="extracted_hash", thumbnail="test_thumb.jpg", play_count=5, is_normalized=True, is_music=True, is_deletable=True, ) test_session.add(sound) await test_session.commit() await test_session.refresh(sound) return sound @pytest.mark.asyncio async def test_download_sound_success( self, authenticated_client: AsyncClient, authenticated_user: User, test_sound: Sound, ) -> None: """Test successful sound download.""" with ( patch("app.api.v1.files.get_sound_file_path") as mock_get_path, tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_file, ): # Setup mock file - write to temp file and close it temp_path = Path(temp_file.name) temp_file.write(b"fake audio data") temp_file.close() mock_get_path.return_value = temp_path try: response = await authenticated_client.get( f"/api/v1/files/sounds/{test_sound.id}/download", ) assert response.status_code == status.HTTP_200_OK finally: # Clean up the temp file temp_path.unlink(missing_ok=True) assert response.headers["content-type"] == "audio/mpeg" assert "attachment" in response.headers.get("content-disposition", "") assert response.content == b"fake audio data" @pytest.mark.asyncio @pytest.mark.asyncio async def test_download_sound_not_found( self, authenticated_client: AsyncClient, authenticated_user: User, ) -> None: """Test download with non-existent sound.""" response = await authenticated_client.get( "/api/v1/files/sounds/99999/download", ) assert response.status_code == status.HTTP_404_NOT_FOUND assert "not found" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_download_sound_file_not_found_on_disk( self, authenticated_client: AsyncClient, authenticated_user: User, test_sound: Sound, ) -> None: """Test download when file doesn't exist on disk.""" with patch("app.api.v1.files.get_sound_file_path") as mock_get_path: # Mock path that doesn't exist mock_get_path.return_value = Path("/non/existent/file.mp3") response = await authenticated_client.get( f"/api/v1/files/sounds/{test_sound.id}/download", ) assert response.status_code == status.HTTP_404_NOT_FOUND assert "not found on disk" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_download_sound_unauthorized( self, client: AsyncClient, test_sound: Sound, ) -> None: """Test download without authentication.""" response = await client.get(f"/api/v1/files/sounds/{test_sound.id}/download") assert response.status_code == status.HTTP_401_UNAUTHORIZED @pytest.mark.asyncio async def test_get_thumbnail_success( self, authenticated_client: AsyncClient, authenticated_user: User, test_sound_with_thumbnail: Sound, ) -> None: """Test successful thumbnail retrieval.""" with tempfile.TemporaryDirectory() as temp_dir: temp_dir_path = Path(temp_dir) # Create the actual thumbnail file thumbnail_path = temp_dir_path / test_sound_with_thumbnail.thumbnail thumbnail_path.write_bytes(b"fake image data") with patch("app.api.v1.files.settings") as mock_settings: mock_settings.EXTRACTION_THUMBNAILS_DIR = temp_dir_path response = await authenticated_client.get( f"/api/v1/files/sounds/{test_sound_with_thumbnail.id}/thumbnail", ) assert response.status_code == status.HTTP_200_OK assert response.headers["content-type"] == "image/jpeg" assert "inline" in response.headers.get("content-disposition", "") assert "Cache-Control" in response.headers @pytest.mark.asyncio async def test_get_thumbnail_no_thumbnail_field( self, authenticated_client: AsyncClient, authenticated_user: User, test_sound: Sound, # Sound without thumbnail ) -> None: """Test thumbnail request for sound without thumbnail.""" response = await authenticated_client.get( f"/api/v1/files/sounds/{test_sound.id}/thumbnail", ) assert response.status_code == status.HTTP_404_NOT_FOUND assert "no thumbnail available" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_get_thumbnail_file_not_found_on_disk( self, authenticated_client: AsyncClient, authenticated_user: User, test_sound_with_thumbnail: Sound, ) -> None: """Test thumbnail request when file doesn't exist on disk.""" with patch("app.core.config.settings") as mock_settings: # Mock directory that doesn't contain the thumbnail mock_settings.EXTRACTION_THUMBNAILS_DIR = "/non/existent/directory" response = await authenticated_client.get( f"/api/v1/files/sounds/{test_sound_with_thumbnail.id}/thumbnail", ) assert response.status_code == status.HTTP_404_NOT_FOUND assert "not found on disk" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_get_thumbnail_unauthorized( self, client: AsyncClient, test_sound_with_thumbnail: Sound, ) -> None: """Test thumbnail request without authentication.""" response = await client.get( f"/api/v1/files/sounds/{test_sound_with_thumbnail.id}/thumbnail", ) assert response.status_code == status.HTTP_401_UNAUTHORIZED @pytest.mark.asyncio async def test_download_normalized_sound( self, authenticated_client: AsyncClient, authenticated_user: User, test_sound_with_thumbnail: Sound, # This sound is normalized ) -> None: """Test downloading a normalized sound returns the normalized file.""" with ( patch("app.api.v1.files.get_sound_file_path") as mock_get_path, tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_file, ): # Setup mock normalized file temp_path = Path(temp_file.name) temp_file.write(b"normalized audio data") temp_file.close() mock_get_path.return_value = temp_path try: response = await authenticated_client.get( f"/api/v1/files/sounds/{test_sound_with_thumbnail.id}/download", ) assert response.status_code == status.HTTP_200_OK assert response.content == b"normalized audio data" # Should use normalized filename mock_get_path.assert_called_once_with(test_sound_with_thumbnail) finally: temp_path.unlink(missing_ok=True)