Files
ripe-cidr-collector/tests/test_daemon.py
T
ayurishchevandClaude Sonnet 5 aaf41adc79 Stage B of review fixes: daemon state lock, image build check, RIPEstat retries
Job state is changed under one RLock, the Dockerfile copies all root
modules and imports them at build time, RIPEstat requests go through a
retrying session with the sourceapp parameter (ripestat_sourceapp) and a
capped Retry-After. Adds the summary and marks review findings 5-10 fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 10:12:48 +03:00

116 lines
5.5 KiB
Python

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
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]