Files
sdb2-backend/tests/test_task_handlers.py
JSC 83239cb4fa Add Alembic for database migrations and initial migration scripts
- Created alembic.ini configuration file for Alembic migrations.
- Added README file for Alembic with a brief description.
- Implemented env.py for Alembic to manage database migrations.
- Created script.py.mako template for migration scripts.
- Added initial migration script to create database tables.
- Created a migration script to add initial plan and playlist data.
- Updated database initialization to run Alembic migrations.
- Enhanced credit service to automatically recharge user credits based on their plan.
- Implemented delete_task method in scheduler service to remove scheduled tasks.
- Updated scheduler API to reflect task deletion instead of cancellation.
- Added CLI tool for managing database migrations.
- Updated tests to cover new functionality for task deletion and credit recharge.
- Updated pyproject.toml and lock files to include Alembic as a dependency.
2025-09-16 13:45:14 +02:00

434 lines
15 KiB
Python

"""Tests for task handlers."""
import uuid
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from sqlmodel.ext.asyncio.session import AsyncSession
from app.models.scheduled_task import ScheduledTask, TaskType
from app.services.task_handlers import TaskExecutionError, TaskHandlerRegistry
class TestTaskHandlerRegistry:
"""Test cases for task handler registry."""
@pytest.fixture
def mock_credit_service(self):
"""Create mock credit service."""
return AsyncMock()
@pytest.fixture
def mock_player_service(self):
"""Create mock player service."""
return AsyncMock()
@pytest.fixture
def task_registry(
self,
db_session: AsyncSession,
mock_credit_service,
mock_player_service,
) -> TaskHandlerRegistry:
"""Create task handler registry fixture."""
def mock_db_session_factory():
return db_session
return TaskHandlerRegistry(
db_session,
mock_db_session_factory,
mock_credit_service,
mock_player_service,
)
async def test_execute_task_unknown_type(
self,
task_registry: TaskHandlerRegistry,
):
"""Test executing task with unknown type."""
# Create task with invalid type
task = ScheduledTask(
name="Unknown Task",
task_type="UNKNOWN_TYPE", # Invalid type
scheduled_at=datetime.now(tz=UTC),
)
with pytest.raises(TaskExecutionError, match="No handler registered"):
await task_registry.execute_task(task)
async def test_handle_credit_recharge_all_users(
self,
task_registry: TaskHandlerRegistry,
mock_credit_service,
):
"""Test handling credit recharge for all users."""
task = ScheduledTask(
name="Daily Credit Recharge",
task_type=TaskType.CREDIT_RECHARGE,
scheduled_at=datetime.now(tz=UTC),
parameters={},
)
mock_credit_service.recharge_all_users_credits.return_value = {
"users_recharged": 10,
"total_credits": 1000,
}
await task_registry.execute_task(task)
mock_credit_service.recharge_all_users_credits.assert_called_once()
async def test_handle_credit_recharge_specific_user(
self,
task_registry: TaskHandlerRegistry,
mock_credit_service,
test_user_id: uuid.UUID,
):
"""Test handling credit recharge for specific user."""
task = ScheduledTask(
name="User Credit Recharge",
task_type=TaskType.CREDIT_RECHARGE,
scheduled_at=datetime.now(tz=UTC),
parameters={"user_id": str(test_user_id)},
)
# Mock transaction object
mock_transaction = MagicMock()
mock_transaction.amount = 100
mock_credit_service.recharge_user_credits_auto.return_value = mock_transaction
await task_registry.execute_task(task)
mock_credit_service.recharge_user_credits_auto.assert_called_once_with(test_user_id)
async def test_handle_credit_recharge_uuid_user_id(
self,
task_registry: TaskHandlerRegistry,
mock_credit_service,
test_user_id: uuid.UUID,
):
"""Test handling credit recharge with UUID user_id parameter."""
task = ScheduledTask(
name="User Credit Recharge",
task_type=TaskType.CREDIT_RECHARGE,
scheduled_at=datetime.now(tz=UTC),
parameters={"user_id": test_user_id}, # UUID object instead of string
)
await task_registry.execute_task(task)
mock_credit_service.recharge_user_credits_auto.assert_called_once_with(test_user_id)
async def test_handle_play_sound_success(
self,
task_registry: TaskHandlerRegistry,
test_sound_id: int,
):
"""Test successful play sound task handling."""
task = ScheduledTask(
name="Play Sound",
task_type=TaskType.PLAY_SOUND,
scheduled_at=datetime.now(tz=UTC),
parameters={"sound_id": str(test_sound_id)},
)
# Mock sound repository
mock_sound = MagicMock()
mock_sound.id = test_sound_id
mock_sound.filename = "test_sound.mp3"
with patch.object(task_registry.sound_repository, "get_by_id", return_value=mock_sound):
with patch("app.services.task_handlers.VLCPlayerService") as mock_vlc_class:
mock_vlc_service = AsyncMock()
mock_vlc_service.play_sound.return_value = True
mock_vlc_class.return_value = mock_vlc_service
await task_registry.execute_task(task)
task_registry.sound_repository.get_by_id.assert_called_once_with(test_sound_id)
mock_vlc_service.play_sound.assert_called_once_with(mock_sound)
async def test_handle_play_sound_missing_sound_id(
self,
task_registry: TaskHandlerRegistry,
):
"""Test play sound task with missing sound_id parameter."""
task = ScheduledTask(
name="Play Sound",
task_type=TaskType.PLAY_SOUND,
scheduled_at=datetime.now(tz=UTC),
parameters={}, # Missing sound_id
)
with pytest.raises(TaskExecutionError, match="sound_id parameter is required"):
await task_registry.execute_task(task)
async def test_handle_play_sound_invalid_sound_id(
self,
task_registry: TaskHandlerRegistry,
):
"""Test play sound task with invalid sound_id parameter."""
task = ScheduledTask(
name="Play Sound",
task_type=TaskType.PLAY_SOUND,
scheduled_at=datetime.now(tz=UTC),
parameters={"sound_id": "invalid-uuid"},
)
with pytest.raises(TaskExecutionError, match="Invalid sound_id format"):
await task_registry.execute_task(task)
async def test_handle_play_sound_not_found(
self,
task_registry: TaskHandlerRegistry,
test_sound_id: int,
):
"""Test play sound task with non-existent sound."""
task = ScheduledTask(
name="Play Sound",
task_type=TaskType.PLAY_SOUND,
scheduled_at=datetime.now(tz=UTC),
parameters={"sound_id": str(test_sound_id)},
)
with patch.object(task_registry.sound_repository, "get_by_id", return_value=None):
with pytest.raises(TaskExecutionError, match="Sound not found"):
await task_registry.execute_task(task)
async def test_handle_play_sound_uuid_parameter(
self,
task_registry: TaskHandlerRegistry,
test_sound_id: int,
):
"""Test play sound task with UUID parameter (not string)."""
task = ScheduledTask(
name="Play Sound",
task_type=TaskType.PLAY_SOUND,
scheduled_at=datetime.now(tz=UTC),
parameters={"sound_id": test_sound_id}, # UUID object
)
mock_sound = MagicMock()
mock_sound.filename = "test_sound.mp3"
with patch.object(task_registry.sound_repository, "get_by_id", return_value=mock_sound):
with patch("app.services.task_handlers.VLCPlayerService") as mock_vlc_class:
mock_vlc_service = AsyncMock()
mock_vlc_class.return_value = mock_vlc_service
await task_registry.execute_task(task)
task_registry.sound_repository.get_by_id.assert_called_once_with(test_sound_id)
async def test_handle_play_playlist_success(
self,
task_registry: TaskHandlerRegistry,
mock_player_service,
test_playlist_id: int,
):
"""Test successful play playlist task handling."""
task = ScheduledTask(
name="Play Playlist",
task_type=TaskType.PLAY_PLAYLIST,
scheduled_at=datetime.now(tz=UTC),
parameters={
"playlist_id": str(test_playlist_id),
"play_mode": "loop",
"shuffle": True,
},
)
# Mock playlist repository
mock_playlist = MagicMock()
mock_playlist.id = test_playlist_id
mock_playlist.name = "Test Playlist"
with patch.object(task_registry.playlist_repository, "get_by_id", return_value=mock_playlist):
await task_registry.execute_task(task)
task_registry.playlist_repository.get_by_id.assert_called_once_with(test_playlist_id)
mock_player_service.load_playlist.assert_called_once_with(test_playlist_id)
mock_player_service.set_mode.assert_called_once_with("loop")
mock_player_service.set_shuffle.assert_called_once_with(shuffle=True)
mock_player_service.play.assert_called_once()
async def test_handle_play_playlist_minimal_parameters(
self,
task_registry: TaskHandlerRegistry,
mock_player_service,
test_playlist_id: int,
):
"""Test play playlist task with minimal parameters."""
task = ScheduledTask(
name="Play Playlist",
task_type=TaskType.PLAY_PLAYLIST,
scheduled_at=datetime.now(tz=UTC),
parameters={"playlist_id": str(test_playlist_id)},
)
mock_playlist = MagicMock()
mock_playlist.name = "Test Playlist"
with patch.object(task_registry.playlist_repository, "get_by_id", return_value=mock_playlist):
await task_registry.execute_task(task)
# Should use default values
mock_player_service.set_mode.assert_called_once_with("continuous")
mock_player_service.set_shuffle.assert_not_called()
async def test_handle_play_playlist_missing_playlist_id(
self,
task_registry: TaskHandlerRegistry,
):
"""Test play playlist task with missing playlist_id parameter."""
task = ScheduledTask(
name="Play Playlist",
task_type=TaskType.PLAY_PLAYLIST,
scheduled_at=datetime.now(tz=UTC),
parameters={}, # Missing playlist_id
)
with pytest.raises(TaskExecutionError, match="playlist_id parameter is required"):
await task_registry.execute_task(task)
async def test_handle_play_playlist_invalid_playlist_id(
self,
task_registry: TaskHandlerRegistry,
):
"""Test play playlist task with invalid playlist_id parameter."""
task = ScheduledTask(
name="Play Playlist",
task_type=TaskType.PLAY_PLAYLIST,
scheduled_at=datetime.now(tz=UTC),
parameters={"playlist_id": "invalid-uuid"},
)
with pytest.raises(TaskExecutionError, match="Invalid playlist_id format"):
await task_registry.execute_task(task)
async def test_handle_play_playlist_not_found(
self,
task_registry: TaskHandlerRegistry,
test_playlist_id: int,
):
"""Test play playlist task with non-existent playlist."""
task = ScheduledTask(
name="Play Playlist",
task_type=TaskType.PLAY_PLAYLIST,
scheduled_at=datetime.now(tz=UTC),
parameters={"playlist_id": str(test_playlist_id)},
)
with patch.object(task_registry.playlist_repository, "get_by_id", return_value=None):
with pytest.raises(TaskExecutionError, match="Playlist not found"):
await task_registry.execute_task(task)
async def test_handle_play_playlist_valid_play_modes(
self,
task_registry: TaskHandlerRegistry,
mock_player_service,
test_playlist_id: int,
):
"""Test play playlist task with various valid play modes."""
mock_playlist = MagicMock()
mock_playlist.name = "Test Playlist"
valid_modes = ["continuous", "loop", "loop_one", "random", "single"]
for mode in valid_modes:
task = ScheduledTask(
name="Play Playlist",
task_type=TaskType.PLAY_PLAYLIST,
scheduled_at=datetime.now(tz=UTC),
parameters={
"playlist_id": str(test_playlist_id),
"play_mode": mode,
},
)
with patch.object(task_registry.playlist_repository, "get_by_id", return_value=mock_playlist):
await task_registry.execute_task(task)
mock_player_service.set_mode.assert_called_with(mode)
# Reset mock for next iteration
mock_player_service.reset_mock()
async def test_handle_play_playlist_invalid_play_mode(
self,
task_registry: TaskHandlerRegistry,
mock_player_service,
test_playlist_id: int,
):
"""Test play playlist task with invalid play mode."""
task = ScheduledTask(
name="Play Playlist",
task_type=TaskType.PLAY_PLAYLIST,
scheduled_at=datetime.now(tz=UTC),
parameters={
"playlist_id": str(test_playlist_id),
"play_mode": "invalid_mode",
},
)
mock_playlist = MagicMock()
mock_playlist.name = "Test Playlist"
with patch.object(task_registry.playlist_repository, "get_by_id", return_value=mock_playlist):
await task_registry.execute_task(task)
# Should not call set_mode for invalid mode
mock_player_service.set_mode.assert_not_called()
# But should still load playlist and play
mock_player_service.load_playlist.assert_called_once()
mock_player_service.play.assert_called_once()
async def test_task_execution_exception_handling(
self,
task_registry: TaskHandlerRegistry,
mock_credit_service,
):
"""Test exception handling during task execution."""
task = ScheduledTask(
name="Failing Task",
task_type=TaskType.CREDIT_RECHARGE,
scheduled_at=datetime.now(tz=UTC),
parameters={},
)
# Make credit service raise an exception
mock_credit_service.recharge_all_users_credits.side_effect = Exception("Service error")
with pytest.raises(TaskExecutionError, match="Task execution failed: Service error"):
await task_registry.execute_task(task)
async def test_task_registry_initialization(
self,
db_session: AsyncSession,
mock_credit_service,
mock_player_service,
):
"""Test task registry initialization."""
def mock_db_session_factory():
return db_session
registry = TaskHandlerRegistry(
db_session,
mock_db_session_factory,
mock_credit_service,
mock_player_service,
)
assert registry.db_session == db_session
assert registry.db_session_factory == mock_db_session_factory
assert registry.credit_service == mock_credit_service
assert registry.player_service == mock_player_service
assert registry.sound_repository is not None
assert registry.playlist_repository is not None
# Check all handlers are registered
expected_handlers = {
TaskType.CREDIT_RECHARGE,
TaskType.PLAY_SOUND,
TaskType.PLAY_PLAYLIST,
}
assert set(registry._handlers.keys()) == expected_handlers