"""Tests for favorite service.""" from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio 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 from app.models.user import User from app.services.favorite import FavoriteService class TestFavoriteService: """Test favorite service operations.""" @pytest_asyncio.fixture async def mock_session_factory(self) -> AsyncGenerator[MagicMock]: """Create a mock session factory.""" mock_session = AsyncMock(spec=AsyncSession) mock_factory = MagicMock(return_value=mock_session) mock_factory.return_value.__aenter__ = AsyncMock(return_value=mock_session) mock_factory.return_value.__aexit__ = AsyncMock(return_value=None) yield mock_factory @pytest_asyncio.fixture async def favorite_service( self, mock_session_factory: MagicMock, ) -> AsyncGenerator[FavoriteService]: """Create a favorite service instance with mocked dependencies.""" yield FavoriteService(mock_session_factory) @pytest_asyncio.fixture async def test_sound(self) -> Sound: """Create a test sound.""" return Sound( id=1, filename="test_sound.mp3", name="Test Sound", type="SDB", duration=5000, size=1024000, hash="abcdef123456789", ) @pytest_asyncio.fixture async def test_playlist(self, test_user: User) -> Playlist: """Create a test playlist.""" return Playlist( id=1, user_id=test_user.id, name="Test Playlist", description="A test playlist", genre="test", is_main=False, is_current=False, is_deletable=True, ) @pytest_asyncio.fixture async def mock_repositories(self) -> dict: """Create mock repositories.""" return { "favorite_repo": AsyncMock(), "user_repo": AsyncMock(), "sound_repo": AsyncMock(), "playlist_repo": AsyncMock(), } @patch("app.services.favorite.socket_manager") @patch("app.services.favorite.FavoriteRepository") @patch("app.services.favorite.UserRepository") @patch("app.services.favorite.SoundRepository") @pytest.mark.asyncio async def test_add_sound_favorite_success( self, mock_sound_repo_class: AsyncMock, mock_user_repo_class: AsyncMock, mock_favorite_repo_class: AsyncMock, mock_socket_manager: AsyncMock, favorite_service: FavoriteService, test_user: User, test_sound: Sound, ) -> None: """Test successfully adding a sound favorite.""" # Setup mocks mock_favorite_repo = AsyncMock() mock_user_repo = AsyncMock() mock_sound_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_user_repo_class.return_value = mock_user_repo mock_sound_repo_class.return_value = mock_sound_repo mock_user_repo.get_by_id.return_value = test_user mock_sound_repo.get_by_id.return_value = test_sound mock_favorite_repo.get_by_user_and_sound.return_value = None expected_favorite = Favorite( id=1, user_id=test_user.id, sound_id=test_sound.id, playlist_id=None, ) mock_favorite_repo.create.return_value = expected_favorite mock_favorite_repo.count_sound_favorites.return_value = 1 # Execute result = await favorite_service.add_sound_favorite(test_user.id, test_sound.id) # Verify assert result == expected_favorite mock_user_repo.get_by_id.assert_called_once_with(test_user.id) mock_sound_repo.get_by_id.assert_called_once_with(test_sound.id) mock_favorite_repo.get_by_user_and_sound.assert_called_once_with(test_user.id, test_sound.id) mock_favorite_repo.create.assert_called_once_with({ "user_id": test_user.id, "sound_id": test_sound.id, "playlist_id": None, }) mock_socket_manager.broadcast_to_all.assert_called_once() @patch("app.services.favorite.UserRepository") @pytest.mark.asyncio async def test_add_sound_favorite_user_not_found( self, mock_user_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test adding sound favorite when user not found.""" mock_user_repo = AsyncMock() mock_user_repo_class.return_value = mock_user_repo mock_user_repo.get_by_id.return_value = None with pytest.raises(ValueError, match="User with ID 1 not found"): await favorite_service.add_sound_favorite(1, 1) @patch("app.services.favorite.SoundRepository") @patch("app.services.favorite.UserRepository") @pytest.mark.asyncio async def test_add_sound_favorite_sound_not_found( self, mock_user_repo_class: AsyncMock, mock_sound_repo_class: AsyncMock, favorite_service: FavoriteService, test_user: User, ) -> None: """Test adding sound favorite when sound not found.""" mock_user_repo = AsyncMock() mock_sound_repo = AsyncMock() mock_user_repo_class.return_value = mock_user_repo mock_sound_repo_class.return_value = mock_sound_repo mock_user_repo.get_by_id.return_value = test_user mock_sound_repo.get_by_id.return_value = None with pytest.raises(ValueError, match="Sound with ID 1 not found"): await favorite_service.add_sound_favorite(test_user.id, 1) @patch("app.services.favorite.FavoriteRepository") @patch("app.services.favorite.SoundRepository") @patch("app.services.favorite.UserRepository") @pytest.mark.asyncio async def test_add_sound_favorite_already_exists( self, mock_user_repo_class: AsyncMock, mock_sound_repo_class: AsyncMock, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, test_user: User, test_sound: Sound, ) -> None: """Test adding sound favorite that already exists.""" mock_user_repo = AsyncMock() mock_sound_repo = AsyncMock() mock_favorite_repo = AsyncMock() mock_user_repo_class.return_value = mock_user_repo mock_sound_repo_class.return_value = mock_sound_repo mock_favorite_repo_class.return_value = mock_favorite_repo mock_user_repo.get_by_id.return_value = test_user mock_sound_repo.get_by_id.return_value = test_sound existing_favorite = Favorite(user_id=test_user.id, sound_id=test_sound.id) mock_favorite_repo.get_by_user_and_sound.return_value = existing_favorite with pytest.raises(ValueError, match="already favorited"): await favorite_service.add_sound_favorite(test_user.id, test_sound.id) @patch("app.services.favorite.FavoriteRepository") @patch("app.services.favorite.PlaylistRepository") @patch("app.services.favorite.UserRepository") @pytest.mark.asyncio async def test_add_playlist_favorite_success( self, mock_user_repo_class: AsyncMock, mock_playlist_repo_class: AsyncMock, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, test_user: User, test_playlist: Playlist, ) -> None: """Test successfully adding a playlist favorite.""" # Setup mocks mock_favorite_repo = AsyncMock() mock_user_repo = AsyncMock() mock_playlist_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_user_repo_class.return_value = mock_user_repo mock_playlist_repo_class.return_value = mock_playlist_repo mock_user_repo.get_by_id.return_value = test_user mock_playlist_repo.get_by_id.return_value = test_playlist mock_favorite_repo.get_by_user_and_playlist.return_value = None expected_favorite = Favorite( id=1, user_id=test_user.id, sound_id=None, playlist_id=test_playlist.id, ) mock_favorite_repo.create.return_value = expected_favorite # Execute result = await favorite_service.add_playlist_favorite(test_user.id, test_playlist.id) # Verify assert result == expected_favorite mock_user_repo.get_by_id.assert_called_once_with(test_user.id) mock_playlist_repo.get_by_id.assert_called_once_with(test_playlist.id) mock_favorite_repo.get_by_user_and_playlist.assert_called_once_with(test_user.id, test_playlist.id) mock_favorite_repo.create.assert_called_once_with({ "user_id": test_user.id, "sound_id": None, "playlist_id": test_playlist.id, }) @patch("app.services.favorite.socket_manager") @patch("app.services.favorite.FavoriteRepository") @patch("app.services.favorite.SoundRepository") @patch("app.services.favorite.UserRepository") @pytest.mark.asyncio async def test_remove_sound_favorite_success( self, mock_user_repo_class: AsyncMock, mock_sound_repo_class: AsyncMock, mock_favorite_repo_class: AsyncMock, mock_socket_manager: AsyncMock, favorite_service: FavoriteService, test_user: User, test_sound: Sound, ) -> None: """Test successfully removing a sound favorite.""" mock_favorite_repo = AsyncMock() mock_user_repo = AsyncMock() mock_sound_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_user_repo_class.return_value = mock_user_repo mock_sound_repo_class.return_value = mock_sound_repo existing_favorite = Favorite(user_id=test_user.id, sound_id=test_sound.id) mock_favorite_repo.get_by_user_and_sound.return_value = existing_favorite mock_user_repo.get_by_id.return_value = test_user mock_sound_repo.get_by_id.return_value = test_sound mock_favorite_repo.count_sound_favorites.return_value = 0 # Execute await favorite_service.remove_sound_favorite(test_user.id, test_sound.id) # Verify mock_favorite_repo.get_by_user_and_sound.assert_called_once_with(test_user.id, test_sound.id) mock_favorite_repo.delete.assert_called_once_with(existing_favorite) mock_socket_manager.broadcast_to_all.assert_called_once() @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_remove_sound_favorite_not_found( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test removing sound favorite that doesn't exist.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_favorite_repo.get_by_user_and_sound.return_value = None with pytest.raises(ValueError, match="is not favorited"): await favorite_service.remove_sound_favorite(1, 1) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_remove_playlist_favorite_success( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, test_user: User, test_playlist: Playlist, ) -> None: """Test successfully removing a playlist favorite.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo existing_favorite = Favorite(user_id=test_user.id, playlist_id=test_playlist.id) mock_favorite_repo.get_by_user_and_playlist.return_value = existing_favorite # Execute await favorite_service.remove_playlist_favorite(test_user.id, test_playlist.id) # Verify mock_favorite_repo.get_by_user_and_playlist.assert_called_once_with(test_user.id, test_playlist.id) mock_favorite_repo.delete.assert_called_once_with(existing_favorite) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_remove_playlist_favorite_not_found( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test removing playlist favorite that doesn't exist.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_favorite_repo.get_by_user_and_playlist.return_value = None with pytest.raises(ValueError, match="is not favorited"): await favorite_service.remove_playlist_favorite(1, 1) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_get_user_favorites( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test getting user favorites.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo expected_favorites = [ Favorite(id=1, user_id=1, sound_id=1), Favorite(id=2, user_id=1, playlist_id=1), ] mock_favorite_repo.get_user_favorites.return_value = expected_favorites # Execute result = await favorite_service.get_user_favorites(1, 10, 0) # Verify assert result == expected_favorites mock_favorite_repo.get_user_favorites.assert_called_once_with(1, 10, 0) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_get_user_sound_favorites( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test getting user sound favorites.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo expected_favorites = [Favorite(id=1, user_id=1, sound_id=1)] mock_favorite_repo.get_user_sound_favorites.return_value = expected_favorites # Execute result = await favorite_service.get_user_sound_favorites(1, 10, 0) # Verify assert result == expected_favorites mock_favorite_repo.get_user_sound_favorites.assert_called_once_with(1, 10, 0) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_get_user_playlist_favorites( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test getting user playlist favorites.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo expected_favorites = [Favorite(id=1, user_id=1, playlist_id=1)] mock_favorite_repo.get_user_playlist_favorites.return_value = expected_favorites # Execute result = await favorite_service.get_user_playlist_favorites(1, 10, 0) # Verify assert result == expected_favorites mock_favorite_repo.get_user_playlist_favorites.assert_called_once_with(1, 10, 0) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_is_sound_favorited( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test checking if sound is favorited.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_favorite_repo.is_sound_favorited.return_value = True # Execute result = await favorite_service.is_sound_favorited(1, 1) # Verify assert result is True mock_favorite_repo.is_sound_favorited.assert_called_once_with(1, 1) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_is_playlist_favorited( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test checking if playlist is favorited.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_favorite_repo.is_playlist_favorited.return_value = False # Execute result = await favorite_service.is_playlist_favorited(1, 1) # Verify assert result is False mock_favorite_repo.is_playlist_favorited.assert_called_once_with(1, 1) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_get_favorite_counts( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test getting favorite counts.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_favorite_repo.count_user_favorites.return_value = 3 mock_favorite_repo.get_user_sound_favorites.return_value = [1, 2] # 2 sounds mock_favorite_repo.get_user_playlist_favorites.return_value = [1] # 1 playlist # Execute result = await favorite_service.get_favorite_counts(1) # Verify expected = { "total": 3, "sounds": 2, "playlists": 1, } assert result == expected @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_get_sound_favorite_count( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test getting sound favorite count.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_favorite_repo.count_sound_favorites.return_value = 5 # Execute result = await favorite_service.get_sound_favorite_count(1) # Verify assert result == 5 mock_favorite_repo.count_sound_favorites.assert_called_once_with(1) @patch("app.services.favorite.FavoriteRepository") @pytest.mark.asyncio async def test_get_playlist_favorite_count( self, mock_favorite_repo_class: AsyncMock, favorite_service: FavoriteService, ) -> None: """Test getting playlist favorite count.""" mock_favorite_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_favorite_repo.count_playlist_favorites.return_value = 3 # Execute result = await favorite_service.get_playlist_favorite_count(1) # Verify assert result == 3 mock_favorite_repo.count_playlist_favorites.assert_called_once_with(1) @patch("app.services.favorite.socket_manager") @patch("app.services.favorite.FavoriteRepository") @patch("app.services.favorite.SoundRepository") @patch("app.services.favorite.UserRepository") @pytest.mark.asyncio async def test_socket_broadcast_error_handling( self, mock_user_repo_class: AsyncMock, mock_sound_repo_class: AsyncMock, mock_favorite_repo_class: AsyncMock, mock_socket_manager: AsyncMock, favorite_service: FavoriteService, test_user: User, test_sound: Sound, ) -> None: """Test that socket broadcast errors don't affect the operation.""" # Setup mocks mock_favorite_repo = AsyncMock() mock_user_repo = AsyncMock() mock_sound_repo = AsyncMock() mock_favorite_repo_class.return_value = mock_favorite_repo mock_user_repo_class.return_value = mock_user_repo mock_sound_repo_class.return_value = mock_sound_repo mock_user_repo.get_by_id.return_value = test_user mock_sound_repo.get_by_id.return_value = test_sound mock_favorite_repo.get_by_user_and_sound.return_value = None expected_favorite = Favorite(id=1, user_id=test_user.id, sound_id=test_sound.id) mock_favorite_repo.create.return_value = expected_favorite mock_favorite_repo.count_sound_favorites.return_value = 1 # Make socket broadcast raise an exception mock_socket_manager.broadcast_to_all.side_effect = Exception("Socket error") # Execute - should not raise exception despite socket error result = await favorite_service.add_sound_favorite(test_user.id, test_sound.id) # Verify operation still succeeded assert result == expected_favorite mock_favorite_repo.create.assert_called_once()