Collector daemon, FastAPI server (addresses, diff, collect, sources), SQLite storage with change journal, Docker Compose deployment, tests, documentation and project rules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
79 lines
3.9 KiB
Python
79 lines
3.9 KiB
Python
import datetime
|
|
import json
|
|
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
|
|
import db
|
|
|
|
|
|
@pytest.fixture
|
|
def files(tmp_path, monkeypatch):
|
|
for attr, name in (("DB_FILE", "ripe.db"), ("DATA_FILE", "data.json"), ("FQDN_DATA_FILE", "fqdn_data.json")):
|
|
monkeypatch.setattr(cc, attr, str(tmp_path / name))
|
|
return tmp_path
|
|
|
|
|
|
def test_legacy_json_import(files):
|
|
before = datetime.datetime.now().isoformat(timespec="seconds")
|
|
(files / "data.json").write_text(json.dumps({
|
|
# Новый формат: first_seen/last_seen сохраняются
|
|
"1": {"last_updated": "2026-02-01T00:00:00", "prefixes": ["1.0.0.0/24"],
|
|
"seen": {"1.0.0.0/24": {"first_seen": "2026-01-01T00:00:00", "last_seen": "2026-02-01T00:00:00"}}},
|
|
# Старый формат без seen: first_seen = last_updated, last_seen = момент миграции
|
|
"2": {"last_updated": "2025-12-01T00:00:00", "prefixes": ["2.0.0.0/24"]}}))
|
|
(files / "fqdn_data.json").write_text(json.dumps(
|
|
{"example.com": {"last_updated": "2025-12-01T00:00:00", "ips": ["9.9.9.9"]}}))
|
|
|
|
with db.session() as conn:
|
|
rows = {(k, s, v): (f, l) for k, s, v, f, l in conn.execute("SELECT * FROM addresses")}
|
|
assert rows[("asn", "1", "1.0.0.0/24")] == ("2026-01-01T00:00:00", "2026-02-01T00:00:00")
|
|
first, last = rows[("asn", "2", "2.0.0.0/24")]
|
|
assert first == "2025-12-01T00:00:00" and last >= before
|
|
assert ("fqdn", "example.com", "9.9.9.9") in rows
|
|
|
|
# Оригиналы переименованы (резервная копия), повторное открытие ничего не импортирует
|
|
assert not (files / "data.json").exists() and len(list(files.glob("data.json.migrated-*"))) == 1
|
|
(files / "data.json").write_text(json.dumps({"3": {"prefixes": ["3.0.0.0/24"]}}))
|
|
with db.session() as conn:
|
|
assert db.count_values(conn) == 3
|
|
|
|
|
|
def test_change_journal(files):
|
|
now = datetime.datetime.now()
|
|
(files / "data.json").write_text(json.dumps({"9": {"prefixes": ["9.0.0.0/24"]}}))
|
|
with db.session() as conn:
|
|
# Импорт из JSON в журнал не попадает
|
|
assert db.get_changes(conn, {"asn"}, cursor=0) == (set(), set(), 0)
|
|
|
|
with db.transaction(conn):
|
|
db.merge_source(conn, "asn", "1", {"a", "b"}, now, 90)
|
|
db.merge_source(conn, "asn", "2", {"b"}, now, 90)
|
|
added, removed, c1 = db.get_changes(conn, {"asn"}, cursor=0)
|
|
assert (added, removed) == ({"a", "b"}, set())
|
|
|
|
# Значение b держит второй источник: исчезает только a
|
|
with db.transaction(conn):
|
|
db.purge_source(conn, "asn", "1")
|
|
assert db.get_changes(conn, {"asn"}, cursor=c1)[:2] == (set(), {"a"})
|
|
|
|
# Удалено и возвращено в одном интервале - изменений нет; повтор с новым курсором пуст
|
|
c2 = db.get_changes(conn, {"asn"}, cursor=c1)[2]
|
|
with db.transaction(conn):
|
|
db.purge_source(conn, "asn", "2")
|
|
db.merge_source(conn, "asn", "2", {"b"}, now, 90)
|
|
added, removed, c3 = db.get_changes(conn, {"asn"}, cursor=c2)
|
|
assert (added, removed) == (set(), set()) and c3 > c2
|
|
assert db.get_changes(conn, {"fqdn"}, cursor=0)[:2] == (set(), set()) # фильтр по типу
|
|
|
|
# Очистка сдвигает горизонт: старый курсор недействителен, текущий - нет; курсор «из будущего» тоже
|
|
with db.transaction(conn):
|
|
assert db.prune_changes(conn, now + datetime.timedelta(days=2), 1) > 0
|
|
assert db.get_changes(conn, {"asn"}, cursor=0) is None
|
|
assert db.get_changes(conn, {"asn"}, cursor=c3 + 1) is None
|
|
assert db.get_changes(conn, {"asn"}, cursor=c3) == (set(), set(), c3)
|