Files
leggen/tests/unit/test_scheduler.py
2025-09-14 21:11:01 +01:00

198 lines
7.2 KiB
Python

"""Tests for background scheduler."""
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from leggen.background.scheduler import BackgroundScheduler
@pytest.mark.unit
class TestBackgroundScheduler:
"""Test background job scheduler."""
@pytest.fixture
def mock_config(self):
"""Mock configuration for scheduler tests."""
return {"sync": {"enabled": True, "hour": 3, "minute": 0, "cron": None}}
@pytest.fixture
def scheduler(self):
"""Create scheduler instance for testing."""
with (
patch("leggen.background.scheduler.SyncService"),
patch("leggen.background.scheduler.config") as mock_config,
):
mock_config.scheduler_config = {
"sync": {"enabled": True, "hour": 3, "minute": 0}
}
# Create scheduler and replace its AsyncIO scheduler with a mock
scheduler = BackgroundScheduler()
mock_scheduler = MagicMock()
mock_scheduler.running = False
mock_scheduler.get_jobs.return_value = []
scheduler.scheduler = mock_scheduler
return scheduler
def test_scheduler_start_default_config(self, scheduler, mock_config):
"""Test starting scheduler with default configuration."""
with patch("leggen.utils.config.config") as mock_config_obj:
mock_config_obj.scheduler_config = mock_config
# Mock the job that gets added
mock_job = MagicMock()
mock_job.id = "daily_sync"
scheduler.scheduler.get_jobs.return_value = [mock_job]
scheduler.start()
# Verify scheduler.start() was called
scheduler.scheduler.start.assert_called_once()
# Verify add_job was called
scheduler.scheduler.add_job.assert_called_once()
def test_scheduler_start_disabled(self, scheduler):
"""Test scheduler behavior when sync is disabled."""
disabled_config = {"sync": {"enabled": False}}
with (
patch.object(scheduler, "scheduler") as mock_scheduler,
patch("leggen.background.scheduler.config") as mock_config_obj,
):
mock_config_obj.scheduler_config = disabled_config
mock_scheduler.running = False
scheduler.start()
# Verify scheduler.start() was called
mock_scheduler.start.assert_called_once()
# Verify add_job was NOT called for disabled sync
mock_scheduler.add_job.assert_not_called()
def test_scheduler_start_with_cron(self, scheduler):
"""Test starting scheduler with custom cron expression."""
cron_config = {
"sync": {
"enabled": True,
"cron": "0 6 * * 1-5", # 6 AM on weekdays
}
}
with patch("leggen.utils.config.config") as mock_config_obj:
mock_config_obj.scheduler_config = cron_config
scheduler.start()
# Verify scheduler.start() and add_job were called
scheduler.scheduler.start.assert_called_once()
scheduler.scheduler.add_job.assert_called_once()
# Verify job was added with correct ID
call_args = scheduler.scheduler.add_job.call_args
assert call_args.kwargs["id"] == "daily_sync"
def test_scheduler_start_invalid_cron(self, scheduler):
"""Test handling of invalid cron expressions."""
invalid_cron_config = {"sync": {"enabled": True, "cron": "invalid cron"}}
with (
patch.object(scheduler, "scheduler") as mock_scheduler,
patch("leggen.background.scheduler.config") as mock_config_obj,
):
mock_config_obj.scheduler_config = invalid_cron_config
mock_scheduler.running = False
scheduler.start()
# With invalid cron, scheduler.start() should not be called due to early return
# and add_job should not be called
mock_scheduler.start.assert_not_called()
mock_scheduler.add_job.assert_not_called()
def test_scheduler_shutdown(self, scheduler):
"""Test scheduler shutdown."""
scheduler.scheduler.running = True
scheduler.shutdown()
scheduler.scheduler.shutdown.assert_called_once()
def test_reschedule_sync(self, scheduler, mock_config):
"""Test rescheduling sync job."""
scheduler.scheduler.running = True
# Reschedule with new config
new_config = {"enabled": True, "hour": 6, "minute": 30}
scheduler.reschedule_sync(new_config)
# Verify remove_job and add_job were called
scheduler.scheduler.remove_job.assert_called_once_with("daily_sync")
scheduler.scheduler.add_job.assert_called_once()
def test_reschedule_sync_disable(self, scheduler, mock_config):
"""Test disabling sync via reschedule."""
scheduler.scheduler.running = True
# Disable sync
disabled_config = {"enabled": False}
scheduler.reschedule_sync(disabled_config)
# Job should be removed but not re-added
scheduler.scheduler.remove_job.assert_called_once_with("daily_sync")
scheduler.scheduler.add_job.assert_not_called()
def test_get_next_sync_time(self, scheduler, mock_config):
"""Test getting next scheduled sync time."""
mock_job = MagicMock()
mock_job.next_run_time = datetime(2025, 9, 2, 3, 0)
scheduler.scheduler.get_job.return_value = mock_job
next_time = scheduler.get_next_sync_time()
assert next_time is not None
assert isinstance(next_time, datetime)
scheduler.scheduler.get_job.assert_called_once_with("daily_sync")
def test_get_next_sync_time_no_job(self, scheduler):
"""Test getting next sync time when no job is scheduled."""
scheduler.scheduler.get_job.return_value = None
next_time = scheduler.get_next_sync_time()
assert next_time is None
scheduler.scheduler.get_job.assert_called_once_with("daily_sync")
@pytest.mark.asyncio
async def test_run_sync_success(self, scheduler):
"""Test successful sync job execution."""
mock_sync_service = AsyncMock()
scheduler.sync_service = mock_sync_service
await scheduler._run_sync()
mock_sync_service.sync_all_accounts.assert_called_once()
@pytest.mark.asyncio
async def test_run_sync_failure(self, scheduler):
"""Test sync job execution with failure."""
mock_sync_service = AsyncMock()
mock_sync_service.sync_all_accounts.side_effect = Exception("Sync failed")
scheduler.sync_service = mock_sync_service
# Should not raise exception, just log error
await scheduler._run_sync()
mock_sync_service.sync_all_accounts.assert_called_once()
def test_scheduler_job_max_instances(self, scheduler, mock_config):
"""Test that sync jobs have max_instances=1."""
with patch("leggen.utils.config.config") as mock_config_obj:
mock_config_obj.scheduler_config = mock_config
scheduler.start()
# Verify add_job was called with max_instances=1
call_args = scheduler.scheduler.add_job.call_args
assert call_args.kwargs["max_instances"] == 1