Files
Aniworld/tests/unit/test_scheduler_config_model.py
Lukas 51b7f349f8 fix(scheduler): strip null legacy alias fields from config.json on save
SchedulerConfig.__init__ maps legacy auto_download/folder_scan keys to the
primary auto_download_after_rescan/folder_scan_enabled fields. However,
model_dump() was including auto_download=null and folder_scan=null in
serialised output. When this was written to config.json and reloaded,
those keys were present (albeit null), so the alias mapping was skipped
and the primary fields retained default False values instead of the
configured True values.

Fix:
- Override SchedulerConfig.model_dump() to drop None-valued alias fields
  before returning the serialised dict.
- ConfigService.save_config() re-serialises the scheduler field through
  its overridden model_dump() so the fix applies when writing to disk.

Tests added:
- test_roundtrip_excludes_none_alias_fields: verifies model_dump omits
  null auto_download/folder_scan keys.
- test_save_and_load_scheduler_flags_roundtrip: end-to-end roundtrip
  through ConfigService confirms raw JSON and loaded values match.

Pre-existing failure in test_core_error_handler.py is unrelated.
2026-05-28 21:18:16 +02:00

260 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 TestSchedulerConfigFolderScanEnabled:
"""3.8 folder_scan_enabled field (Task 1.1)."""
def test_default_folder_scan_enabled(self) -> None:
config = SchedulerConfig()
assert config.folder_scan_enabled is False
def test_set_folder_scan_enabled_true(self) -> None:
config = SchedulerConfig(folder_scan_enabled=True)
assert config.folder_scan_enabled is True
def test_set_folder_scan_enabled_false(self) -> None:
config = SchedulerConfig(folder_scan_enabled=False)
assert config.folder_scan_enabled is False
def test_backward_compat_missing_field(self) -> None:
"""Old configs without folder_scan_enabled load successfully."""
dumped = {
"enabled": True,
"interval_minutes": 60,
"schedule_time": "03:00",
"schedule_days": ALL_DAYS,
"auto_download_after_rescan": False,
}
config = SchedulerConfig(**dumped)
assert config.folder_scan_enabled is False
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
assert config.folder_scan_enabled is False
def test_legacy_auto_download_false(self) -> None:
config = SchedulerConfig(auto_download=False)
assert config.auto_download_after_rescan is False
def test_legacy_folder_scan_true(self) -> None:
"""Legacy folder_scan=true maps to folder_scan_enabled=True."""
config = SchedulerConfig(folder_scan=True)
assert config.folder_scan_enabled is True
assert config.auto_download_after_rescan is False
def test_legacy_folder_scan_false(self) -> None:
config = SchedulerConfig(folder_scan=False)
assert config.folder_scan_enabled is False
def test_legacy_both_set(self) -> None:
"""Both legacy keys can be set simultaneously."""
config = SchedulerConfig(auto_download=True, folder_scan=True)
assert config.auto_download_after_rescan is True
assert config.folder_scan_enabled 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,
folder_scan=True,
folder_scan_enabled=True,
)
# Both set to True — no conflict possible when both agree
assert config.auto_download_after_rescan is True
assert config.folder_scan_enabled 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,
"folder_scan": True,
}
config = SchedulerConfig(**data)
assert config.auto_download_after_rescan is True
assert config.folder_scan_enabled 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,
folder_scan_enabled=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,
folder_scan_enabled=True,
)
dumped = original.model_dump()
# Alias fields must not appear when None
assert "auto_download" not in dumped
assert "folder_scan" not in dumped
# Primary fields roundtrip correctly
restored = SchedulerConfig(**dumped)
assert restored.auto_download_after_rescan is True
assert restored.folder_scan_enabled is True