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 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()