Files
ripe-cidr-collector/tests/test_daemon.py
T

115 lines
5.5 KiB
Python
Raw Normal View History

2026-09-21 07:29:38 +03:00
import datetime
import os
import sys
import pytest
from fastapi.testclient import TestClient
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import api_server
import cidr_collector as cc
import collector_daemon as daemon
import db
2026-09-21 07:29:38 +03:00
from storage import load_json, save_json_atomic
@pytest.fixture
def env(tmp_path, monkeypatch):
for attr, name in (("CONFIG_FILE", "config.json"), ("DATA_FILE", "data.json"),
("FQDN_DATA_FILE", "fqdn_data.json"), ("STATUS_FILE", "status.json"),
("DB_FILE", "ripe.db"), ("COLLECT_REQUEST_FILE", "collect_request.json")):
monkeypatch.setattr(cc, attr, str(tmp_path / name))
monkeypatch.setattr(daemon, "job_state", {name: {"cron": None, "last_run": None, "last_finished": None,
"running": False, "last_error": None,
"rejected_cron": None} for name in daemon.COLLECTORS})
return tmp_path
def test_sync_schedule_reschedules_and_ignores_invalid_cron(env):
save_json_atomic(cc.CONFIG_FILE, {"schedule": {"asn": "0 1 * * *", "fqdn": "0 3 * * *"}})
scheduler = daemon.build_scheduler()
assert daemon.job_state["asn"]["cron"] == "0 1 * * *"
save_json_atomic(cc.CONFIG_FILE, {"schedule": {"asn": "*/5 * * * *", "fqdn": "0 3 * * *"}})
daemon.sync_schedule(scheduler)
assert daemon.job_state["asn"]["cron"] == "*/5 * * * *"
assert "*/5" in str(scheduler.get_job("asn_job").trigger)
# Невалидный cron не ломает действующее расписание
save_json_atomic(cc.CONFIG_FILE, {"schedule": {"asn": "not a cron", "fqdn": "0 3 * * *"}})
daemon.sync_schedule(scheduler)
assert daemon.job_state["asn"]["cron"] == "*/5 * * * *"
# Каждая сверка пишет heartbeat
assert load_json(cc.STATUS_FILE, None)["jobs"]["asn"]["cron"] == "*/5 * * * *"
def test_health_reflects_collector_heartbeat(env):
client = TestClient(api_server.app)
# Демон не запускался
body = client.get("/health").json()
assert body["collector_alive"] is False and body["status"] == "degraded"
now = datetime.datetime.now()
save_json_atomic(cc.STATUS_FILE, {"updated_at": now.isoformat(), "jobs": {}})
body = client.get("/health").json()
assert body["collector_alive"] is True and body["status"] == "ok"
stale = now - datetime.timedelta(seconds=cc.STATUS_STALE_AFTER + 60)
save_json_atomic(cc.STATUS_FILE, {"updated_at": stale.isoformat(), "jobs": {}})
body = client.get("/health").json()
assert body["collector_alive"] is False and body["status"] == "degraded"
def test_collect_request_api(env, monkeypatch):
monkeypatch.setenv("RIPE_API_TOKEN", "secret")
client = TestClient(api_server.app)
key = {"X-API-Key": "secret"}
assert client.post("/collect").status_code == 401
# Демон не запущен: запрос не ставится в очередь
assert client.post("/collect", headers=key).status_code == 503
assert not os.path.exists(cc.COLLECT_REQUEST_FILE)
save_json_atomic(cc.STATUS_FILE, {"updated_at": datetime.datetime.now().isoformat(), "jobs": {}})
assert client.post("/collect", json={"type": "asn"}, headers=key).json()["requested"] == ["asn"]
# Повторные запросы объединяются; без тела - все типы
assert client.post("/collect", headers=key).json()["requested"] == ["asn", "fqdn"]
assert client.post("/collect", json={"type": "bad"}, headers=key).status_code == 422
assert load_json(cc.COLLECT_REQUEST_FILE, {})["types"] == ["asn", "fqdn"]
def test_daemon_runs_requested_collection_once(env, monkeypatch):
scheduler = daemon.build_scheduler()
cc.request_collection(["asn"])
daemon.check_collect_requests(scheduler)
assert scheduler.get_job("asn_manual") is not None and scheduler.get_job("fqdn_manual") is None
assert not os.path.exists(cc.COLLECT_REQUEST_FILE) # запрос забран
daemon.check_collect_requests(scheduler) # повторный вызов ничего не делает
# Пока сбор этого типа идёт, второй запуск пропускается
monkeypatch.setitem(daemon.COLLECTORS, "asn", lambda: pytest.fail("collection must be skipped"))
daemon._run_locks["asn"].acquire()
try:
daemon.run_job("asn", scheduler)
finally:
daemon._run_locks["asn"].release()
def test_backup_job_rotates_and_is_registered():
# Задание backup зарегистрировано рядом с asn и fqdn и использует расписание по умолчанию
assert daemon.COLLECTORS["backup"] is daemon.run_backup and "backup" in daemon.DEFAULT_CRONS
assert "backup" in daemon.job_state
with open(cc.CONFIG_FILE, "w") as f:
f.write('{"asns": [], "fqdns": [], "backup_keep": 2}')
now = datetime.datetime.now()
with db.session() as conn:
for days in (1, 2, 3):
db.backup_database(conn, now - datetime.timedelta(days=days), keep=10)
daemon.run_backup() # keep=2 из config.json: остаются свежая копия и самая новая из старых
backups = db.list_backups(cc.BACKUP_DIR)
yesterday = (now - datetime.timedelta(days=1)).astimezone(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
assert len(backups) == 2 and backups[1].endswith(f"ripe-{yesterday}.db") and backups[0] > backups[1]