Refactor code structure for improved readability and maintainability
Some checks failed
Backend CI / lint (push) Failing after 4m51s
Backend CI / test (push) Failing after 4m35s

This commit is contained in:
JSC
2025-08-20 11:37:28 +02:00
parent 9653062003
commit 821093f64f
15 changed files with 1897 additions and 217 deletions

View File

@@ -0,0 +1,548 @@
"""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()