"""Тесты хранилища настроек админки: ссылка, интервал, история входов.""" import json import pytest from schedule import ScheduleError from schedule.settings import ( DEFAULT_INTERVAL_MINUTES, SettingsStore, clamp_interval, interval_from_env, ) def test_defaults(): store = SettingsStore() assert store.link == "" assert store.interval_minutes == DEFAULT_INTERVAL_MINUTES assert store.last_attempt is None assert store.last_success is None def test_persist_and_reload(tmp_path): path = tmp_path / "settings.json" store = SettingsStore(link="l1", interval_minutes=20, path=path) store.set_link("l2") store.set_interval_minutes(45) again = SettingsStore(link="из-окружения", interval_minutes=1, path=path) assert again.link == "l2" assert again.interval_minutes == 45 def test_file_overrides_env(tmp_path): path = tmp_path / "settings.json" path.write_text(json.dumps({"link": "из-файла", "interval_minutes": 5}), encoding="utf-8") store = SettingsStore(link="из-окружения", interval_minutes=99, path=path) assert store.link == "из-файла" assert store.interval_minutes == 5 def test_interval_validation(): store = SettingsStore() for bad in (0, 1441, -5, "abc"): with pytest.raises(ScheduleError): store.set_interval_minutes(bad) assert store.set_interval_minutes("30") == 30 def test_empty_link_rejected(): store = SettingsStore() with pytest.raises(ScheduleError): store.set_link(" ") with pytest.raises(ScheduleError): store.set_link("") def test_corrupt_file_is_tolerated(tmp_path): path = tmp_path / "settings.json" path.write_text("{это не json", encoding="utf-8") store = SettingsStore(link="env-link", interval_minutes=7, path=path) assert store.link == "env-link" assert store.interval_minutes == 7 def test_login_history_saved(tmp_path): path = tmp_path / "settings.json" store = SettingsStore(link="l", path=path) store.record_login("10.0.0.1", ok=False) assert store.last_attempt["ip"] == "10.0.0.1" assert store.last_attempt["ok"] is False assert store.last_attempt["at"] assert store.last_success is None # успешных ещё не было store.record_login("10.0.0.2", ok=True) assert store.last_success["ip"] == "10.0.0.2" # история переживает перезапуск: читаем тем же путём again = SettingsStore(path=path) assert again.last_attempt["ip"] == "10.0.0.2" assert again.last_attempt["ok"] is True assert again.last_success["ip"] == "10.0.0.2" def test_unwritable_path_marks_save_error(tmp_path): blocker = tmp_path / "blocker" blocker.write_text("x", encoding="utf-8") store = SettingsStore(link="l", interval_minutes=3, path=blocker / "settings.json") assert store.set_interval_minutes(10) == 10 # в памяти меняется assert store.interval_minutes == 10 assert store.save_error # и видно, что на диск не сохранилось def test_default_settings_path_is_inside_project(): """Путь по умолчанию вычисляется от корня проекта, а не от глубины файла. В docker-образе файлы лежат плоско (/app/...), поэтому глубину не считаем. """ from schedule.settings import DEFAULT_SETTINGS_PATH assert DEFAULT_SETTINGS_PATH.name == "settings.json" assert DEFAULT_SETTINGS_PATH.parent.name == "data" root = DEFAULT_SETTINGS_PATH.parent.parent assert (root / "requirements.txt").is_file() # корень найден по маркеру def test_clamp_interval(): assert clamp_interval(0) == 1 assert clamp_interval(99999) == 1440 assert clamp_interval(30) == 30 def test_interval_from_env(monkeypatch): monkeypatch.delenv("SCHEDULE_INTERVAL_MINUTES", raising=False) monkeypatch.delenv("SCHEDULE_CACHE_TTL", raising=False) assert interval_from_env() == DEFAULT_INTERVAL_MINUTES # старый SCHEDULE_CACHE_TTL (в секундах) продолжает работать monkeypatch.setenv("SCHEDULE_CACHE_TTL", "900") assert interval_from_env() == 15 monkeypatch.setenv("SCHEDULE_INTERVAL_MINUTES", "7") assert interval_from_env() == 7