Withhold addresses after the database is recreated without a backup
When ripe.db is corrupted and no valid backup exists, a new empty database is created with a db_recreated.json marker; /addresses and /addresses/diff answer 503 until the collector gathers data again, /health reports db_recreated. Tests now isolate all state files via conftest. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46d56654d8
commit
0155fd3f38
13 files changed
+226
-32
No files matched your search
@@ -0,0 +1,18 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import cidr_collector as cc
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_state(tmp_path, monkeypatch):
|
||||
"""Файлы состояния всех тестов - во временном каталоге: ни один тест не трогает каталог проекта и не видит чужие метки."""
|
||||
for attr, name in (("CONFIG_FILE", "config.json"), ("DB_FILE", "ripe.db"), ("DATA_FILE", "data.json"),
|
||||
("FQDN_DATA_FILE", "fqdn_data.json"), ("STATUS_FILE", "status.json"),
|
||||
("COLLECT_REQUEST_FILE", "collect_request.json"), ("RESTORE_FILE", "last_restore.json"),
|
||||
("RECREATED_FILE", "db_recreated.json"), ("BACKUP_DIR", "backups")):
|
||||
monkeypatch.setattr(cc, attr, str(tmp_path / name))
|
||||
+4
-4
@@ -56,12 +56,12 @@ def test_corrupted_storage_is_preserved(files):
|
||||
assert not path.exists()
|
||||
assert len(list(files.glob("data.json.corrupt-*"))) == 1
|
||||
|
||||
# Битая база: убирается в сторону, ошибка отдаётся как StorageError
|
||||
# Битая база без копий: оригинал убран в сторону, создана новая пустая база и поставлена метка пересоздания
|
||||
(files / "ripe.db").write_bytes(b"this is not a sqlite database" * 100)
|
||||
with pytest.raises(StorageError):
|
||||
db.connect()
|
||||
assert not (files / "ripe.db").exists()
|
||||
with db.session() as conn:
|
||||
assert db.get_values(conn) == []
|
||||
assert len(list(files.glob("ripe.db.corrupt-*"))) == 1
|
||||
assert (files / "db_recreated.json").exists()
|
||||
|
||||
|
||||
def test_post_schedule_auth(files, monkeypatch):
|
||||
|
||||
+23
-3
@@ -19,6 +19,8 @@ def files(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(cc, attr, str(tmp_path / name))
|
||||
monkeypatch.setattr(cc, "BACKUP_DIR", str(tmp_path / "backups"))
|
||||
monkeypatch.setattr(cc, "RESTORE_FILE", str(tmp_path / "last_restore.json"))
|
||||
monkeypatch.setattr(cc, "RECREATED_FILE", str(tmp_path / "db_recreated.json"))
|
||||
monkeypatch.setattr(cc, "CONFIG_FILE", str(tmp_path / "config.json"))
|
||||
return tmp_path
|
||||
|
||||
|
||||
@@ -121,9 +123,27 @@ def test_restore_from_backup(files):
|
||||
assert len(glob.glob(str(files / "ripe.db.corrupt-*"))) == 1
|
||||
assert json.loads((files / "last_restore.json").read_text())["backup"].endswith(".db")
|
||||
|
||||
# Без исправных копий - прежнее поведение: ошибка хранилища, база в карантине
|
||||
# Без исправных копий создаётся новая пустая база (с меткой, см. test_recreated_database_guard)
|
||||
for backup in db.list_backups(cc.BACKUP_DIR):
|
||||
os.unlink(backup)
|
||||
(files / "ripe.db").write_bytes(b"garbage" * 1000)
|
||||
with pytest.raises(db.StorageError):
|
||||
db.connect()
|
||||
with db.session() as conn:
|
||||
assert db.get_values(conn) == []
|
||||
assert os.path.exists(cc.RECREATED_FILE) and glob.glob(str(files / "ripe.db.corrupt-*"))
|
||||
|
||||
|
||||
def test_recreated_database_guard(files):
|
||||
(files / "config.json").write_text('{"asns": [1], "fqdns": []}')
|
||||
(files / "ripe.db").write_bytes(b"garbage" * 1000) # порча, копий нет
|
||||
|
||||
with db.session() as conn:
|
||||
assert os.path.exists(cc.RECREATED_FILE)
|
||||
# ASN настроен, данных нет - ожидание; FQDN не настроен - пустой список законен
|
||||
assert db.recreated_pending(conn, ("asn",)) and not db.recreated_pending(conn, ("fqdn",))
|
||||
assert db.get_changes(conn, {"asn"}, cursor=5) is None # курсор старой базы недействителен
|
||||
|
||||
# Данные собраны заново - сборщик снимает метку
|
||||
with db.transaction(conn):
|
||||
db.merge_source(conn, "asn", "1", {"a"}, datetime.datetime.now(), 90)
|
||||
db.settle_recreated(conn)
|
||||
assert not os.path.exists(cc.RECREATED_FILE)
|
||||
@@ -21,6 +21,9 @@ def env(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(cc, "DATA_FILE", str(tmp_path / "data.json"))
|
||||
monkeypatch.setattr(cc, "FQDN_DATA_FILE", str(tmp_path / "fqdn_data.json"))
|
||||
monkeypatch.setattr(cc, "DB_FILE", str(tmp_path / "ripe.db"))
|
||||
monkeypatch.setattr(cc, "RECREATED_FILE", str(tmp_path / "db_recreated.json"))
|
||||
monkeypatch.setattr(cc, "RESTORE_FILE", str(tmp_path / "last_restore.json"))
|
||||
monkeypatch.setattr(cc, "BACKUP_DIR", str(tmp_path / "backups"))
|
||||
monkeypatch.setenv("RIPE_API_TOKEN", "secret")
|
||||
save_json_atomic(cc.CONFIG_FILE, {"asns": [], "fqdns": [], "ttl_days": 90})
|
||||
return TestClient(api_server.app)
|
||||
@@ -106,3 +109,26 @@ def test_input_hardening(env):
|
||||
assert env.get("/addresses/diff", params={"since": bad}).status_code == 400, bad
|
||||
# Корректный, но выходящий за журнал курсор - 410, а не 400
|
||||
assert env.get("/addresses/diff", params={"since": "9" * 30}).status_code == 410
|
||||
|
||||
|
||||
def test_recreated_database_is_withheld(env):
|
||||
cc.add_to_config_list("asns", 62041)
|
||||
with open(cc.DB_FILE, "wb") as f: # порча базы, копий нет
|
||||
f.write(b"garbage" * 1000)
|
||||
|
||||
# Пока данные не собраны заново, пустой список не отдаётся (503 + Retry-After)
|
||||
for url in ("/addresses", "/addresses/diff?since=0"):
|
||||
response = env.get(url)
|
||||
assert response.status_code == 503 and response.headers["Retry-After"], url
|
||||
assert env.get("/addresses", params={"type": "fqdn"}).json() == [] # тип без источников не блокируется
|
||||
health = env.get("/health").json()
|
||||
assert health["status"] == "degraded" and health["db_recreated"]["pending"] is True
|
||||
assert "quarantine" not in health["db_recreated"] # пути наружу не отдаются
|
||||
|
||||
# Сборщик собрал данные: выдача возобновляется, метка снята
|
||||
with db.session() as conn:
|
||||
with db.transaction(conn):
|
||||
db.merge_source(conn, "asn", "62041", {"1.0.0.0/24"}, datetime.datetime.now(), 90)
|
||||
db.settle_recreated(conn)
|
||||
assert env.get("/addresses").json() == ["1.0.0.0/24"]
|
||||
assert env.get("/health").json()["db_recreated"] is None
|
||||
Reference in new issue
Block a user