Add comprehensive tests for scheduled task repository, scheduler service, and task handlers
- Implemented tests for ScheduledTaskRepository covering task creation, retrieval, filtering, and status updates. - Developed tests for SchedulerService including task creation, cancellation, user task retrieval, and maintenance jobs. - Created tests for TaskHandlerRegistry to validate task execution for various types, including credit recharge and sound playback. - Ensured proper error handling and edge cases in task execution scenarios. - Added fixtures and mocks to facilitate isolated testing of services and repositories.
This commit is contained in:
424
tests/test_task_handlers.py
Normal file
424
tests/test_task_handlers.py
Normal file
@@ -0,0 +1,424 @@
|
||||
"""Tests for task handlers."""
|
||||
|
||||
import uuid
|
||||
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 MagicMock()
|
||||
|
||||
@pytest.fixture
|
||||
def task_registry(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
mock_credit_service,
|
||||
mock_player_service,
|
||||
) -> TaskHandlerRegistry:
|
||||
"""Create task handler registry fixture."""
|
||||
return TaskHandlerRegistry(
|
||||
db_session,
|
||||
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.utcnow(),
|
||||
)
|
||||
|
||||
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.utcnow(),
|
||||
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.utcnow(),
|
||||
parameters={"user_id": str(test_user_id)},
|
||||
)
|
||||
|
||||
mock_credit_service.recharge_user_credits.return_value = {
|
||||
"user_id": str(test_user_id),
|
||||
"credits_added": 100,
|
||||
}
|
||||
|
||||
await task_registry.execute_task(task)
|
||||
|
||||
mock_credit_service.recharge_user_credits.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.utcnow(),
|
||||
parameters={"user_id": test_user_id}, # UUID object instead of string
|
||||
)
|
||||
|
||||
await task_registry.execute_task(task)
|
||||
|
||||
mock_credit_service.recharge_user_credits.assert_called_once_with(test_user_id)
|
||||
|
||||
async def test_handle_play_sound_success(
|
||||
self,
|
||||
task_registry: TaskHandlerRegistry,
|
||||
test_sound_id: uuid.UUID,
|
||||
):
|
||||
"""Test successful play sound task handling."""
|
||||
task = ScheduledTask(
|
||||
name="Play Sound",
|
||||
task_type=TaskType.PLAY_SOUND,
|
||||
scheduled_at=datetime.utcnow(),
|
||||
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.vlc_player.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)
|
||||
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.utcnow(),
|
||||
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.utcnow(),
|
||||
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: uuid.UUID,
|
||||
):
|
||||
"""Test play sound task with non-existent sound."""
|
||||
task = ScheduledTask(
|
||||
name="Play Sound",
|
||||
task_type=TaskType.PLAY_SOUND,
|
||||
scheduled_at=datetime.utcnow(),
|
||||
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: uuid.UUID,
|
||||
):
|
||||
"""Test play sound task with UUID parameter (not string)."""
|
||||
task = ScheduledTask(
|
||||
name="Play Sound",
|
||||
task_type=TaskType.PLAY_SOUND,
|
||||
scheduled_at=datetime.utcnow(),
|
||||
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.vlc_player.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: uuid.UUID,
|
||||
):
|
||||
"""Test successful play playlist task handling."""
|
||||
task = ScheduledTask(
|
||||
name="Play Playlist",
|
||||
task_type=TaskType.PLAY_PLAYLIST,
|
||||
scheduled_at=datetime.utcnow(),
|
||||
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(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: uuid.UUID,
|
||||
):
|
||||
"""Test play playlist task with minimal parameters."""
|
||||
task = ScheduledTask(
|
||||
name="Play Playlist",
|
||||
task_type=TaskType.PLAY_PLAYLIST,
|
||||
scheduled_at=datetime.utcnow(),
|
||||
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_called_once_with(False)
|
||||
|
||||
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.utcnow(),
|
||||
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.utcnow(),
|
||||
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: uuid.UUID,
|
||||
):
|
||||
"""Test play playlist task with non-existent playlist."""
|
||||
task = ScheduledTask(
|
||||
name="Play Playlist",
|
||||
task_type=TaskType.PLAY_PLAYLIST,
|
||||
scheduled_at=datetime.utcnow(),
|
||||
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: uuid.UUID,
|
||||
):
|
||||
"""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.utcnow(),
|
||||
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: uuid.UUID,
|
||||
):
|
||||
"""Test play playlist task with invalid play mode."""
|
||||
task = ScheduledTask(
|
||||
name="Play Playlist",
|
||||
task_type=TaskType.PLAY_PLAYLIST,
|
||||
scheduled_at=datetime.utcnow(),
|
||||
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.utcnow(),
|
||||
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."""
|
||||
registry = TaskHandlerRegistry(
|
||||
db_session,
|
||||
mock_credit_service,
|
||||
mock_player_service,
|
||||
)
|
||||
|
||||
assert registry.db_session == db_session
|
||||
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
|
||||
Reference in New Issue
Block a user