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>
116 lines
5.5 KiB
Python
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]
|