integrity_scanner_fuer_stat.../tests/test_baseline.py

225 lines
8.9 KiB
Python
Raw Permalink Normal View History

"""Tests for snapshot saving, baseline management and approval workflow."""
import json
import pytest
from pathlib import Path
from scanner.baseline import BaselineManager, url_key
def _sample_page(url: str, text: str = "Inhalt") -> dict:
return {
"url": url,
"final_url": url,
"status": 200,
"content_type": "text/html",
"response_headers": {},
"text": text,
"links": {"a": [], "canonical": None, "meta_refresh": []},
"metadata": {"title": "T", "h1": [], "h2": []},
"hidden_content": [],
"inline_scripts": [],
"jsonld": [],
"comment_links": [],
}
@pytest.fixture
def bm(tmp_path: Path) -> BaselineManager:
return BaselineManager(tmp_path / "data")
class TestUrlKey:
def test_deterministic(self):
assert url_key("https://example.com/") == url_key("https://example.com/")
def test_different_urls_different_keys(self):
assert url_key("https://a.com/") != url_key("https://b.com/")
def test_16_hex_chars(self):
k = url_key("https://example.com/")
assert len(k) == 16
assert all(c in "0123456789abcdef" for c in k)
class TestSaveSnapshot:
def test_creates_manifest(self, bm, tmp_path):
pages = [_sample_page("https://b.info/")]
snap_dir = bm.save_snapshot(pages)
assert (snap_dir / "manifest.json").exists()
def test_manifest_content(self, bm):
pages = [_sample_page("https://b.info/"), _sample_page("https://b.info/p/")]
snap_dir = bm.save_snapshot(pages)
manifest = json.loads((snap_dir / "manifest.json").read_text())
assert manifest["page_count"] == 2
assert "https://b.info/" in manifest["urls"]
def test_creates_page_files(self, bm):
pages = [_sample_page("https://b.info/")]
snap_dir = bm.save_snapshot(pages)
page_files = list((snap_dir / "pages").glob("*.json"))
assert len(page_files) == 1
def test_latest_snapshot_dir(self, bm):
assert bm.latest_snapshot_dir() is None
bm.save_snapshot([_sample_page("https://b.info/")])
assert bm.latest_snapshot_dir() is not None
class TestLoadSnapshot:
def test_loads_saved_snapshot(self, bm):
pages = [_sample_page("https://b.info/"), _sample_page("https://b.info/p/")]
bm.save_snapshot(pages)
snap = bm.load_snapshot()
assert "https://b.info/" in snap["pages"]
assert "https://b.info/p/" in snap["pages"]
def test_returns_empty_when_no_snapshot(self, bm):
result = bm.load_snapshot()
assert result == {}
class TestBaselineExists:
def test_false_before_approve(self, bm):
assert not bm.baseline_exists()
def test_true_after_approve(self, bm):
bm.save_snapshot([_sample_page("https://b.info/")])
bm.approve_all(note="test")
assert bm.baseline_exists()
class TestApproveAll:
def test_approve_all_creates_baseline(self, bm):
pages = [_sample_page("https://b.info/"), _sample_page("https://b.info/p/")]
bm.save_snapshot(pages)
approved = bm.approve_all(note="Initial baseline")
assert "https://b.info/" in approved
assert "https://b.info/p/" in approved
assert bm.baseline_exists()
def test_manifest_contains_approval_metadata(self, bm):
bm.save_snapshot([_sample_page("https://b.info/")])
bm.approve_all(note="test note", approved_by="testuser")
manifest = json.loads((bm.baseline_dir / "manifest.json").read_text())
assert manifest["note"] == "test note"
assert manifest["approved_by"] == "testuser"
assert "approved_at" in manifest
def test_approve_all_raises_without_snapshot(self, bm):
with pytest.raises(RuntimeError):
bm.approve_all()
class TestApproveUrl:
def test_approve_single_url(self, bm):
pages = [_sample_page("https://b.info/"), _sample_page("https://b.info/p/")]
bm.save_snapshot(pages)
result = bm.approve_url("https://b.info/")
assert result is True
assert bm.baseline_exists()
baseline = bm.load_baseline()
assert "https://b.info/" in baseline["pages"]
assert "https://b.info/p/" not in baseline["pages"]
def test_approve_missing_url_returns_false(self, bm):
bm.save_snapshot([_sample_page("https://b.info/")])
result = bm.approve_url("https://b.info/does-not-exist/")
assert result is False
class TestLoadBaseline:
def test_loads_approved_pages(self, bm):
pages = [_sample_page("https://b.info/", "Text A")]
bm.save_snapshot(pages)
bm.approve_all()
baseline = bm.load_baseline()
assert "https://b.info/" in baseline["pages"]
assert baseline["pages"]["https://b.info/"]["text"] == "Text A"
def test_never_auto_updates(self, bm):
"""Baseline must not change unless approve is explicitly called."""
bm.save_snapshot([_sample_page("https://b.info/", "Original")])
bm.approve_all()
# Second snapshot with different content
bm.save_snapshot([_sample_page("https://b.info/", "Manipuliert")])
# Baseline must still contain original content
baseline = bm.load_baseline()
assert baseline["pages"]["https://b.info/"]["text"] == "Original"
class TestRebuildBaseline:
def test_rebuild_replaces_baseline(self, bm):
bm.save_snapshot([_sample_page("https://b.info/", "Alt")])
bm.approve_all()
bm.save_snapshot([_sample_page("https://b.info/", "Neu")])
bm.rebuild_baseline(note="Major update")
baseline = bm.load_baseline()
assert baseline["pages"]["https://b.info/"]["text"] == "Neu"
# ---------------------------------------------------------------------------
# AI content inventory (SQLite store: verdict + description + url→hash)
# ---------------------------------------------------------------------------
def _entry(kind="image", url="https://b.info/x.jpg", category="clean", desc="Ein Logo"):
return {"kind": kind, "url": url, "description": desc, "category": category,
"severity": "none", "confidence": 0.9, "explanation": "ok",
"model": "m", "dismissed": False, "checked_at": "2026-01-01T00:00:00"}
class TestAiInventory:
def test_roundtrip_preserves_entries_and_url_hashes(self, tmp_path):
bm = BaselineManager(tmp_path)
bm.save_ai_ledger({"entries": {"H1": _entry(desc="Schützenfest-Plakat")},
"url_hashes": {"https://b.info/x.jpg": "H1"}})
led = bm.load_ai_ledger()
assert led["entries"]["H1"]["description"] == "Schützenfest-Plakat"
assert led["entries"]["H1"]["dismissed"] is False
assert led["url_hashes"] == {"https://b.info/x.jpg": "H1"}
def test_migrates_json_ledger_once(self, tmp_path):
(tmp_path).mkdir(parents=True, exist_ok=True)
old = tmp_path / "ai_ledger.json"
old.write_text(json.dumps({"entries": {"H1": _entry()}, "url_hashes": {"u": "H1"}}),
encoding="utf-8")
bm = BaselineManager(tmp_path)
led = bm.load_ai_ledger()
assert "H1" in led["entries"]
assert not old.exists() # umbenannt
assert (tmp_path / "ai_ledger.json.migrated").exists()
assert (tmp_path / "content_inventory.db").exists()
def test_first_seen_preserved_last_seen_updated(self, tmp_path):
bm = BaselineManager(tmp_path)
bm.save_ai_ledger({"entries": {"H1": _entry()}, "url_hashes": {}})
rows1 = bm.query_inventory()
first1, last1 = rows1[0]["first_seen"], rows1[0]["last_seen"]
import time; time.sleep(0.01)
bm.save_ai_ledger({"entries": {"H1": _entry(category="propaganda")}, "url_hashes": {}})
rows2 = bm.query_inventory()
assert rows2[0]["first_seen"] == first1 # bleibt
assert rows2[0]["last_seen"] >= last1 # aktualisiert
assert rows2[0]["category"] == "propaganda" # Feld aktualisiert
def test_invalidate_removes_url(self, tmp_path):
bm = BaselineManager(tmp_path)
bm.save_ai_ledger({"entries": {}, "url_hashes": {"a": "H1", "b": "H2"}})
assert bm.invalidate_ai_url_hashes(["a"]) == 1
assert bm.load_ai_ledger()["url_hashes"] == {"b": "H2"}
def test_query_search_and_csv_export(self, tmp_path):
bm = BaselineManager(tmp_path)
bm.save_ai_ledger({"entries": {
"H1": _entry(url="https://b.info/kloster.jpg", desc="Foto des Klosters Bredelar"),
"H2": _entry(url="https://b.info/auto.jpg", desc="Ein Auto"),
}, "url_hashes": {}})
hits = bm.query_inventory(search="kloster")
assert len(hits) == 1 and "Kloster" in hits[0]["description"]
out = tmp_path / "inv.csv"
assert bm.export_inventory_csv(out) == 2
content = out.read_text(encoding="utf-8")
assert "description" in content and "Kloster" in content