202 lines
7.0 KiB
Python
202 lines
7.0 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 TestSchedulerConfigLegacyAliases:
|
||
"""3.10 – Legacy config key aliases (auto_download, folder_scan)."""
|
||
|
||
def test_legacy_auto_download_true(self) -> None:
|
||
"""Legacy auto_download=true maps to auto_download_after_rescan=True."""
|
||
config = SchedulerConfig(auto_download=True)
|
||
assert config.auto_download_after_rescan is True
|
||
|
||
def test_explicit_primary_overrides_legacy(self) -> None:
|
||
"""Primary field explicitly set to False still wins over legacy True.
|
||
|
||
When user provides both old and new key, newer key wins by virtue of
|
||
being the intended migration target. Legacy alias only applies when
|
||
primary key is absent from data entirely.
|
||
"""
|
||
config = SchedulerConfig(
|
||
auto_download=True,
|
||
auto_download_after_rescan=True,
|
||
)
|
||
# Both set to True — no conflict possible when both agree
|
||
assert config.auto_download_after_rescan is True
|
||
|
||
def test_explicit_primary_false_wins_over_legacy_true(self) -> None:
|
||
"""Primary=False explicitly set wins over legacy=True.
|
||
|
||
User has migrated config to new keys but old key still present.
|
||
Explicit primary value must be respected.
|
||
"""
|
||
config = SchedulerConfig(
|
||
auto_download=True,
|
||
auto_download_after_rescan=False,
|
||
)
|
||
assert config.auto_download_after_rescan is False
|
||
|
||
def test_explicit_primary_true_wins_over_legacy_false(self) -> None:
|
||
"""Primary=True explicitly set wins over legacy=False."""
|
||
config = SchedulerConfig(
|
||
auto_download=False,
|
||
auto_download_after_rescan=True,
|
||
)
|
||
assert config.auto_download_after_rescan is True
|
||
|
||
def test_legacy_in_json_dict(self) -> None:
|
||
"""Simulate config.json with legacy auto_download key."""
|
||
data = {
|
||
"enabled": True,
|
||
"schedule_time": "03:00",
|
||
"schedule_days": ALL_DAYS,
|
||
"auto_download": True,
|
||
}
|
||
config = SchedulerConfig(**data)
|
||
assert config.auto_download_after_rescan is True
|
||
|
||
|
||
class TestSchedulerConfigSerialisation:
|
||
"""3.9 – 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
|
||
|
||
def test_roundtrip_excludes_none_alias_fields(self) -> None:
|
||
"""model_dump must not emit null auto_download/folder_scan keys.
|
||
|
||
Previously these null keys were written to config.json on save.
|
||
On reload they were present (even as None), so the alias mapping in
|
||
__init__ was skipped and the primary fields retained their default
|
||
False values instead of the configured True values.
|
||
"""
|
||
original = SchedulerConfig(
|
||
auto_download_after_rescan=True,
|
||
)
|
||
dumped = original.model_dump()
|
||
# Alias fields must not appear when None
|
||
assert "auto_download" not in dumped
|
||
# Primary fields roundtrip correctly
|
||
restored = SchedulerConfig(**dumped)
|
||
assert restored.auto_download_after_rescan is True
|