"""Tests for favorite API endpoints.""" from contextlib import suppress import pytest import pytest_asyncio from httpx import AsyncClient from sqlmodel.ext.asyncio.session import AsyncSession from app.models.favorite import Favorite from app.models.playlist import Playlist from app.models.sound import Sound class TestFavoriteEndpoints: """Test favorite API endpoints.""" @pytest_asyncio.fixture async def test_sound( self, test_session: AsyncSession, ) -> Sound: """Create a test sound.""" sound = Sound( filename="test_sound.mp3", name="Test Sound", type="SDB", duration=5000, # 5 seconds in ms size=1024000, # 1MB hash="abcdef123456789", ) test_session.add(sound) await test_session.commit() await test_session.refresh(sound) return sound @pytest_asyncio.fixture async def test_sound2( self, test_session: AsyncSession, ) -> Sound: """Create a second test sound.""" sound = Sound( filename="test_sound2.mp3", name="Test Sound 2", type="TTS", duration=3000, # 3 seconds in ms size=512000, # 512KB hash="xyz789456123abc", ) test_session.add(sound) await test_session.commit() await test_session.refresh(sound) return sound @pytest_asyncio.fixture async def test_playlist( self, test_session: AsyncSession, ) -> Playlist: """Create a test playlist.""" playlist = Playlist( user_id=1, # Use hardcoded ID name="Test Playlist", description="A test playlist", genre="test", is_main=False, is_current=False, is_deletable=True, ) test_session.add(playlist) await test_session.commit() await test_session.refresh(playlist) return playlist @pytest_asyncio.fixture async def test_playlist2( self, test_session: AsyncSession, ) -> Playlist: """Create a second test playlist.""" playlist = Playlist( user_id=1, # Use hardcoded ID name="Test Playlist 2", description="Another test playlist", genre="test2", is_main=False, is_current=False, is_deletable=True, ) test_session.add(playlist) await test_session.commit() await test_session.refresh(playlist) return playlist @pytest_asyncio.fixture async def existing_sound_favorite( self, test_session: AsyncSession, ) -> Favorite: """Create an existing sound favorite.""" favorite = Favorite( user_id=1, # Use hardcoded ID sound_id=1, # Use hardcoded ID ) test_session.add(favorite) await test_session.commit() await test_session.refresh(favorite) return favorite @pytest_asyncio.fixture async def existing_playlist_favorite( self, test_session: AsyncSession, ) -> Favorite: """Create an existing playlist favorite.""" favorite = Favorite( user_id=1, # Use hardcoded ID playlist_id=1, # Use hardcoded ID ) test_session.add(favorite) await test_session.commit() await test_session.refresh(favorite) return favorite @pytest.mark.asyncio async def test_add_sound_favorite_success( self, authenticated_client: AsyncClient, test_sound: Sound, ) -> None: """Test successfully adding a sound to favorites.""" # Clean up any existing favorite first with suppress(Exception): await authenticated_client.delete("/api/v1/favorites/sounds/1") response = await authenticated_client.post("/api/v1/favorites/sounds/1") assert response.status_code == 200 data = response.json() assert "id" in data assert data["sound_id"] == 1 assert data["playlist_id"] is None assert "created_at" in data assert "updated_at" in data @pytest.mark.asyncio async def test_add_sound_favorite_not_found( self, authenticated_client: AsyncClient, ) -> None: """Test adding a favorite for non-existent sound.""" response = await authenticated_client.post("/api/v1/favorites/sounds/99999") assert response.status_code == 404 assert "Sound with ID 99999 not found" in response.json()["detail"] @pytest.mark.asyncio async def test_add_sound_favorite_already_exists( self, authenticated_client: AsyncClient, existing_sound_favorite: Favorite, test_sound: Sound, ) -> None: """Test adding a sound favorite that already exists.""" response = await authenticated_client.post(f"/api/v1/favorites/sounds/{test_sound.id}") assert response.status_code == 409 assert "already favorited" in response.json()["detail"] @pytest.mark.asyncio async def test_add_playlist_favorite_success( self, authenticated_client: AsyncClient, test_playlist: Playlist, ) -> None: """Test successfully adding a playlist to favorites.""" # Clean up any existing favorite first with suppress(Exception): await authenticated_client.delete("/api/v1/favorites/playlists/1") response = await authenticated_client.post("/api/v1/favorites/playlists/1") assert response.status_code == 200 data = response.json() assert "id" in data assert data["sound_id"] is None assert data["playlist_id"] == 1 assert "created_at" in data assert "updated_at" in data @pytest.mark.asyncio async def test_add_playlist_favorite_not_found( self, authenticated_client: AsyncClient, ) -> None: """Test adding a favorite for non-existent playlist.""" response = await authenticated_client.post("/api/v1/favorites/playlists/99999") assert response.status_code == 404 assert "Playlist with ID 99999 not found" in response.json()["detail"] @pytest.mark.asyncio async def test_add_playlist_favorite_already_exists( self, authenticated_client: AsyncClient, existing_playlist_favorite: Favorite, test_playlist: Playlist, ) -> None: """Test adding a playlist favorite that already exists.""" response = await authenticated_client.post(f"/api/v1/favorites/playlists/{test_playlist.id}") assert response.status_code == 409 assert "already favorited" in response.json()["detail"] @pytest.mark.asyncio async def test_remove_sound_favorite_success( self, authenticated_client: AsyncClient, existing_sound_favorite: Favorite, test_sound: Sound, ) -> None: """Test successfully removing a sound from favorites.""" response = await authenticated_client.delete(f"/api/v1/favorites/sounds/{test_sound.id}") assert response.status_code == 200 data = response.json() assert data["message"] == "Sound removed from favorites" @pytest.mark.asyncio async def test_remove_sound_favorite_not_found( self, authenticated_client: AsyncClient, test_sound: Sound, ) -> None: """Test removing a sound favorite that doesn't exist.""" response = await authenticated_client.delete(f"/api/v1/favorites/sounds/{test_sound.id}") assert response.status_code == 404 assert "is not favorited" in response.json()["detail"] @pytest.mark.asyncio async def test_remove_playlist_favorite_success( self, authenticated_client: AsyncClient, existing_playlist_favorite: Favorite, test_playlist: Playlist, ) -> None: """Test successfully removing a playlist from favorites.""" response = await authenticated_client.delete(f"/api/v1/favorites/playlists/{test_playlist.id}") assert response.status_code == 200 data = response.json() assert data["message"] == "Playlist removed from favorites" @pytest.mark.asyncio async def test_remove_playlist_favorite_not_found( self, authenticated_client: AsyncClient, test_playlist: Playlist, ) -> None: """Test removing a playlist favorite that doesn't exist.""" response = await authenticated_client.delete(f"/api/v1/favorites/playlists/{test_playlist.id}") assert response.status_code == 404 assert "is not favorited" in response.json()["detail"] @pytest.mark.asyncio async def test_get_all_favorites_empty( self, authenticated_client: AsyncClient, ) -> None: """Test getting all favorites when user has none.""" # First get all existing favorites for the test user response = await authenticated_client.get("/api/v1/favorites/") assert response.status_code == 200 existing_data = response.json() # Clean up any existing favorites for this test user for favorite in existing_data["favorites"]: if favorite.get("sound_id"): await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") elif favorite.get("playlist_id"): await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") # Now test that the user has no favorites response = await authenticated_client.get("/api/v1/favorites/") assert response.status_code == 200 data = response.json() assert data["favorites"] == [] @pytest.mark.asyncio async def test_get_all_favorites_with_data( self, authenticated_client: AsyncClient, existing_sound_favorite: Favorite, existing_playlist_favorite: Favorite, ) -> None: """Test getting all favorites with mixed data.""" # Clean up any existing favorites first response = await authenticated_client.get("/api/v1/favorites/") assert response.status_code == 200 existing_data = response.json() for favorite in existing_data["favorites"]: if favorite.get("sound_id"): await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") elif favorite.get("playlist_id"): await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") # Add the test favorites using hardcoded IDs await authenticated_client.post("/api/v1/favorites/sounds/1") await authenticated_client.post("/api/v1/favorites/playlists/1") response = await authenticated_client.get("/api/v1/favorites/") assert response.status_code == 200 data = response.json() assert len(data["favorites"]) == 2 # Check that we have both types sound_favorite = next((f for f in data["favorites"] if f["sound_id"] is not None), None) playlist_favorite = next((f for f in data["favorites"] if f["playlist_id"] is not None), None) assert sound_favorite is not None assert playlist_favorite is not None assert sound_favorite["sound_id"] == 1 assert playlist_favorite["playlist_id"] == 1 @pytest.mark.asyncio async def test_get_all_favorites_pagination( self, authenticated_client: AsyncClient, test_session: AsyncSession, ) -> None: """Test pagination for get all favorites.""" # Create multiple favorites favorite1 = Favorite(user_id=1, sound_id=1) favorite2 = Favorite(user_id=1, sound_id=2) test_session.add(favorite1) test_session.add(favorite2) await test_session.commit() # Test limit response = await authenticated_client.get("/api/v1/favorites/?limit=1") assert response.status_code == 200 data = response.json() assert len(data["favorites"]) == 1 # Test offset response = await authenticated_client.get("/api/v1/favorites/?offset=1&limit=1") assert response.status_code == 200 data = response.json() assert len(data["favorites"]) == 1 @pytest.mark.asyncio async def test_get_sound_favorites( self, authenticated_client: AsyncClient, existing_sound_favorite: Favorite, existing_playlist_favorite: Favorite, ) -> None: """Test getting only sound favorites.""" # Clean up any existing favorites first response = await authenticated_client.get("/api/v1/favorites/") assert response.status_code == 200 existing_data = response.json() for favorite in existing_data["favorites"]: if favorite.get("sound_id"): await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") elif favorite.get("playlist_id"): await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") # Add test favorites using hardcoded IDs await authenticated_client.post("/api/v1/favorites/sounds/1") await authenticated_client.post("/api/v1/favorites/playlists/1") response = await authenticated_client.get("/api/v1/favorites/sounds") assert response.status_code == 200 data = response.json() assert len(data["favorites"]) == 1 assert data["favorites"][0]["sound_id"] == 1 assert data["favorites"][0]["playlist_id"] is None @pytest.mark.asyncio async def test_get_playlist_favorites( self, authenticated_client: AsyncClient, existing_sound_favorite: Favorite, existing_playlist_favorite: Favorite, ) -> None: """Test getting only playlist favorites.""" # Clean up any existing favorites first response = await authenticated_client.get("/api/v1/favorites/") assert response.status_code == 200 existing_data = response.json() for favorite in existing_data["favorites"]: if favorite.get("sound_id"): await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") elif favorite.get("playlist_id"): await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") # Add test favorites using hardcoded IDs await authenticated_client.post("/api/v1/favorites/sounds/1") await authenticated_client.post("/api/v1/favorites/playlists/1") response = await authenticated_client.get("/api/v1/favorites/playlists") assert response.status_code == 200 data = response.json() assert len(data["favorites"]) == 1 assert data["favorites"][0]["playlist_id"] == 1 assert data["favorites"][0]["sound_id"] is None @pytest.mark.asyncio async def test_get_favorite_counts( self, authenticated_client: AsyncClient, existing_sound_favorite: Favorite, existing_playlist_favorite: Favorite, ) -> None: """Test getting favorite counts.""" # Clean up any existing favorites first response = await authenticated_client.get("/api/v1/favorites/") assert response.status_code == 200 existing_data = response.json() for favorite in existing_data["favorites"]: if favorite.get("sound_id"): await authenticated_client.delete(f"/api/v1/favorites/sounds/{favorite['sound_id']}") elif favorite.get("playlist_id"): await authenticated_client.delete(f"/api/v1/favorites/playlists/{favorite['playlist_id']}") # Add test favorites using hardcoded IDs await authenticated_client.post("/api/v1/favorites/sounds/1") await authenticated_client.post("/api/v1/favorites/playlists/1") response = await authenticated_client.get("/api/v1/favorites/counts") assert response.status_code == 200 data = response.json() assert data["total"] == 2 assert data["sounds"] == 1 assert data["playlists"] == 1 @pytest.mark.asyncio async def test_check_sound_favorited_true( self, authenticated_client: AsyncClient, existing_sound_favorite: Favorite, test_sound: Sound, ) -> None: """Test checking if a sound is favorited (true case).""" # Add a favorite for sound ID 1 using hardcoded ID await authenticated_client.post("/api/v1/favorites/sounds/1") response = await authenticated_client.get("/api/v1/favorites/sounds/1/check") assert response.status_code == 200 data = response.json() assert data["is_favorited"] is True @pytest.mark.asyncio async def test_check_sound_favorited_false( self, authenticated_client: AsyncClient, test_sound: Sound, ) -> None: """Test checking if a sound is favorited (false case).""" # Make sure sound 1 is not favorited with suppress(Exception): await authenticated_client.delete("/api/v1/favorites/sounds/1") response = await authenticated_client.get("/api/v1/favorites/sounds/1/check") assert response.status_code == 200 data = response.json() assert data["is_favorited"] is False @pytest.mark.asyncio async def test_check_playlist_favorited_true( self, authenticated_client: AsyncClient, existing_playlist_favorite: Favorite, test_playlist: Playlist, ) -> None: """Test checking if a playlist is favorited (true case).""" # Add a favorite for playlist ID 1 using hardcoded ID await authenticated_client.post("/api/v1/favorites/playlists/1") response = await authenticated_client.get("/api/v1/favorites/playlists/1/check") assert response.status_code == 200 data = response.json() assert data["is_favorited"] is True @pytest.mark.asyncio async def test_check_playlist_favorited_false( self, authenticated_client: AsyncClient, test_playlist: Playlist, ) -> None: """Test checking if a playlist is favorited (false case).""" # Make sure playlist 1 is not favorited with suppress(Exception): await authenticated_client.delete("/api/v1/favorites/playlists/1") response = await authenticated_client.get("/api/v1/favorites/playlists/1/check") assert response.status_code == 200 data = response.json() assert data["is_favorited"] is False @pytest.mark.asyncio async def test_endpoints_require_authentication( self, test_client: AsyncClient, test_sound: Sound, test_playlist: Playlist, ) -> None: """Test that all endpoints require authentication.""" # Use hardcoded IDs to avoid session context issues endpoints = [ ("GET", "/api/v1/favorites/"), ("GET", "/api/v1/favorites/sounds"), ("GET", "/api/v1/favorites/playlists"), ("GET", "/api/v1/favorites/counts"), ("POST", "/api/v1/favorites/sounds/1"), ("POST", "/api/v1/favorites/playlists/1"), ("DELETE", "/api/v1/favorites/sounds/1"), ("DELETE", "/api/v1/favorites/playlists/1"), ("GET", "/api/v1/favorites/sounds/1/check"), ("GET", "/api/v1/favorites/playlists/1/check"), ] for method, endpoint in endpoints: if method == "GET": response = await test_client.get(endpoint) elif method == "POST": response = await test_client.post(endpoint) elif method == "DELETE": response = await test_client.delete(endpoint) assert response.status_code == 401, f"Endpoint {method} {endpoint} should require authentication" @pytest.mark.asyncio async def test_query_parameter_validation( self, authenticated_client: AsyncClient, ) -> None: """Test query parameter validation.""" # Test invalid limit (too small) response = await authenticated_client.get("/api/v1/favorites/?limit=0") assert response.status_code == 422 # Test invalid limit (too large) response = await authenticated_client.get("/api/v1/favorites/?limit=101") assert response.status_code == 422 # Test invalid offset (negative) response = await authenticated_client.get("/api/v1/favorites/?offset=-1") assert response.status_code == 422 # Test valid parameters response = await authenticated_client.get("/api/v1/favorites/?limit=50&offset=0") assert response.status_code == 200