- Replace asyncio sleep loop with APScheduler AsyncIOScheduler + CronTrigger
- Add schedule_time (HH:MM), schedule_days (days of week), auto_download_after_rescan fields to SchedulerConfig
- Add _auto_download_missing() to queue missing episodes after rescan
- Reload config live via reload_config(SchedulerConfig) without restart
- Update GET/POST /api/scheduler/config to return {success, config, status} envelope
- Add day-of-week pill toggles to Settings -> Scheduler section in UI
- Update JS loadSchedulerConfig / saveSchedulerConfig for new API shape
- Add 29 unit tests for SchedulerConfig model, 18 unit tests for SchedulerService
- Rewrite 23 endpoint tests and 36 integration tests for APScheduler behaviour
- Coverage: 96% api/scheduler, 95% scheduler_service, 90% total (>= 80% threshold)
- Update docs: API.md, CONFIGURATION.md, features.md, CHANGELOG.md
130 lines
4.1 KiB
Python
130 lines
4.1 KiB
Python
"""Unit tests for SchedulerConfig model fields and validators (Task 3)."""
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
|
||
from src.server.models.config import SchedulerConfig
|
||
|
||
ALL_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||
|
||
|
||
class TestSchedulerConfigDefaults:
|
||
"""3.1 – Default values."""
|
||
|
||
def test_default_schedule_time(self) -> None:
|
||
config = SchedulerConfig()
|
||
assert config.schedule_time == "03:00"
|
||
|
||
def test_default_schedule_days(self) -> None:
|
||
config = SchedulerConfig()
|
||
assert config.schedule_days == ALL_DAYS
|
||
|
||
def test_default_auto_download(self) -> None:
|
||
config = SchedulerConfig()
|
||
assert config.auto_download_after_rescan is False
|
||
|
||
def test_default_enabled(self) -> None:
|
||
config = SchedulerConfig()
|
||
assert config.enabled is True
|
||
|
||
def test_default_interval_minutes(self) -> None:
|
||
config = SchedulerConfig()
|
||
assert config.interval_minutes == 60
|
||
|
||
|
||
class TestSchedulerConfigValidScheduleTime:
|
||
"""3.2 – Valid schedule_time values."""
|
||
|
||
@pytest.mark.parametrize("time_val", ["00:00", "03:00", "12:30", "23:59"])
|
||
def test_valid_times(self, time_val: str) -> None:
|
||
config = SchedulerConfig(schedule_time=time_val)
|
||
assert config.schedule_time == time_val
|
||
|
||
|
||
class TestSchedulerConfigInvalidScheduleTime:
|
||
"""3.3 – Invalid schedule_time values must raise ValidationError."""
|
||
|
||
@pytest.mark.parametrize(
|
||
"time_val",
|
||
["25:00", "3pm", "", "3:00pm", "24:00", "-1:00", "9:00", "1:60"],
|
||
)
|
||
def test_invalid_times(self, time_val: str) -> None:
|
||
with pytest.raises(ValidationError):
|
||
SchedulerConfig(schedule_time=time_val)
|
||
|
||
|
||
class TestSchedulerConfigValidScheduleDays:
|
||
"""3.4 – Valid schedule_days values."""
|
||
|
||
def test_single_day(self) -> None:
|
||
config = SchedulerConfig(schedule_days=["mon"])
|
||
assert config.schedule_days == ["mon"]
|
||
|
||
def test_multiple_days(self) -> None:
|
||
config = SchedulerConfig(schedule_days=["mon", "fri"])
|
||
assert config.schedule_days == ["mon", "fri"]
|
||
|
||
def test_all_days(self) -> None:
|
||
config = SchedulerConfig(schedule_days=ALL_DAYS)
|
||
assert config.schedule_days == ALL_DAYS
|
||
|
||
def test_empty_list(self) -> None:
|
||
config = SchedulerConfig(schedule_days=[])
|
||
assert config.schedule_days == []
|
||
|
||
|
||
class TestSchedulerConfigInvalidScheduleDays:
|
||
"""3.5 – Invalid schedule_days values must raise ValidationError."""
|
||
|
||
@pytest.mark.parametrize(
|
||
"days",
|
||
[
|
||
["monday"],
|
||
["xyz"],
|
||
["Mon"], # Case-sensitive — must be lowercase
|
||
[""],
|
||
],
|
||
)
|
||
def test_invalid_days(self, days: list) -> None:
|
||
with pytest.raises(ValidationError):
|
||
SchedulerConfig(schedule_days=days)
|
||
|
||
|
||
class TestSchedulerConfigAutoDownload:
|
||
"""3.6 – auto_download_after_rescan field."""
|
||
|
||
def test_set_true(self) -> None:
|
||
config = SchedulerConfig(auto_download_after_rescan=True)
|
||
assert config.auto_download_after_rescan is True
|
||
|
||
def test_set_false(self) -> None:
|
||
config = SchedulerConfig(auto_download_after_rescan=False)
|
||
assert config.auto_download_after_rescan is False
|
||
|
||
|
||
class TestSchedulerConfigBackwardCompat:
|
||
"""3.7 – Backward compatibility: old fields still work."""
|
||
|
||
def test_legacy_fields_use_defaults(self) -> None:
|
||
config = SchedulerConfig(enabled=True, interval_minutes=30)
|
||
assert config.schedule_time == "03:00"
|
||
assert config.schedule_days == ALL_DAYS
|
||
assert config.auto_download_after_rescan is False
|
||
assert config.enabled is True
|
||
assert config.interval_minutes == 30
|
||
|
||
|
||
class TestSchedulerConfigSerialisation:
|
||
"""3.8 – Serialisation roundtrip."""
|
||
|
||
def test_roundtrip(self) -> None:
|
||
original = SchedulerConfig(
|
||
enabled=True,
|
||
interval_minutes=120,
|
||
schedule_time="04:30",
|
||
schedule_days=["mon", "wed", "fri"],
|
||
auto_download_after_rescan=True,
|
||
)
|
||
dumped = original.model_dump()
|
||
restored = SchedulerConfig(**dumped)
|
||
assert restored == original
|