"""Typed settings accessors and form validation.""" import pytest from ytstream import settings as settings_module from ytstream.settings import DEFAULTS, Settings, validate, validate_all class TestAccessors: def test_missing_key_returns_the_default(self, settings): assert settings.get_int("retention_days") == 30 assert settings.get_int("min_keep_videos") == 5 assert settings.get_str("proxy_base_url") == "http://127.0.0.1:8099" def test_unknown_key_never_raises(self, settings): assert settings.get_str("no_such_key") == "" assert settings.get_int("no_such_key") == 0 def test_set_then_get(self, settings): settings.set("retention_days", "30") assert settings.get_int("retention_days") == 30 def test_set_overwrites(self, settings): settings.set("retention_days", "30") settings.set("retention_days", "45") assert settings.get_int("retention_days") == 45 def test_corrupt_integer_falls_back_to_the_default(self, settings): settings.set("retention_days", "not a number") assert settings.get_int("retention_days") == 30 def test_reads_are_not_cached(self, conn, settings): """Rotating the API key must be nothing more than saving a new value — no restart of the hourly job or the admin server.""" settings.set("youtube_api_key", "first") assert settings.get_str("youtube_api_key") == "first" # A second Settings object standing in for the other process. other = Settings(conn) other.set("youtube_api_key", "second") assert settings.get_str("youtube_api_key") == "second" def test_all_editable_covers_every_default(self, settings): assert set(settings.all_editable()) == set(DEFAULTS) def test_secrets_are_not_editable(self): for key in settings_module.SECRET_KEYS: assert key not in settings_module.EDITABLE def test_both_api_keys_are_masked(self): assert "youtube_api_key" in settings_module.MASKED_KEYS assert "jellyfin_api_key" in settings_module.MASKED_KEYS def test_download_era_settings_are_gone(self): for gone in ("disk_cap_gb", "write_subs", "sub_langs", "sponsorblock_mark", "max_attempts", "backfill_days"): assert gone not in DEFAULTS class TestValidation: @pytest.mark.parametrize( "key, value", [ ("retention_days", "30"), ("min_keep_videos", "0"), ("min_keep_videos", "5"), ("backfill_max_videos", "300"), ("max_height", "1080"), ("min_duration_seconds", "120"), ("subsync_max_new", "25"), ("subsync_missing_threshold", "3"), ("jellyfin_url", "http://127.0.0.1:8096"), ("pot_provider_url", "https://example.com:4416"), ("proxy_base_url", "http://127.0.0.1:8099"), ("youtube_api_key", ""), ], ) def test_accepts_good_values(self, key, value): ok, _ = validate(key, value) assert ok @pytest.mark.parametrize( "key, value", [ ("retention_days", "abc"), ("retention_days", "0"), ("retention_days", "-5"), ("min_keep_videos", "-1"), ("max_height", "10"), ("min_duration_seconds", "-1"), ("jellyfin_url", "not-a-url"), ("jellyfin_url", "ftp://host/"), ("jellyfin_url", "http://"), ("proxy_base_url", "nonsense"), ], ) def test_rejects_bad_values(self, key, value): ok, message = validate(key, value) assert not ok assert message def test_zero_missing_threshold_is_rejected(self): """Zero would unsubscribe on the first absent response, which is exactly the failure the threshold exists to prevent.""" ok, message = validate("subsync_missing_threshold", "0") assert not ok assert message def test_validate_all_reports_each_bad_field(self): errors = validate_all( {"retention_days": "abc", "max_height": "1080", "jellyfin_url": "nope"} ) assert set(errors) == {"retention_days", "jellyfin_url"} def test_validate_all_ignores_unknown_keys(self): assert validate_all({"not_a_setting": "x"}) == {} def test_empty_api_keys_are_acceptable(self): assert validate("jellyfin_api_key", "")[0] assert validate("youtube_api_key", "")[0] def test_whitespace_is_tolerated(self): ok, _ = validate("retention_days", " 21 ") assert ok