"""Audit ledger: append-only, hash-chained, tamper-evident (Article 8).""" from __future__ import annotations import json from tests.helpers import ( HEX64_RE, json_text, ledger_append, ledger_entries, ledger_verify, open_ledger, ) def _fresh_ledger(tmp_path): path = tmp_path / "ledger.jsonl" return path, open_ledger(path) def test_append_and_read_back(tmp_path): path, lgr = _fresh_ledger(tmp_path) ledger_append(lgr, "test.event", {"marker": "genesis-marker-AAA"}) ledger_append(lgr, "test.event", {"marker": "second-marker-BBB"}) ledger_append(lgr, "vote.cast", {"citizen": "cit-alice", "choice": "yes"}) entries = ledger_entries(lgr) assert len(entries) == 3 combined = " ".join(json_text(e) for e in entries) assert "genesis-marker-AAA" in combined assert "second-marker-BBB" in combined assert "cit-alice" in combined def test_entries_persist_across_reopen(tmp_path): path, lgr = _fresh_ledger(tmp_path) ledger_append(lgr, "test.event", {"n": 1}) ledger_append(lgr, "test.event", {"n": 2}) reopened = open_ledger(path) assert len(ledger_entries(reopened)) == 2 def test_intact_ledger_verifies(tmp_path): path, lgr = _fresh_ledger(tmp_path) for index in range(5): ledger_append(lgr, "test.event", {"index": index}) verdict = ledger_verify(open_ledger(path)) assert verdict in (True, None) if verdict is None: raise AssertionError("ledger exposes no verify-like method; Article 8 requires one") def test_ledger_file_contains_hash_chain(tmp_path): path, lgr = _fresh_ledger(tmp_path) ledger_append(lgr, "test.event", {"marker": "one"}) ledger_append(lgr, "test.event", {"marker": "two"}) assert path.exists(), ( "expected the ledger to be written at the path it was opened with; " "adjust open_ledger in tests/helpers.py if the layout differs" ) text = path.read_text(encoding="utf-8") hashes = set(HEX64_RE.findall(text)) assert len(hashes) >= 2, "expected at least two distinct sha256 hex digests in the chain" def test_tampered_payload_is_detected(tmp_path): path, lgr = _fresh_ledger(tmp_path) ledger_append(lgr, "test.event", {"marker": "honest-value"}) ledger_append(lgr, "test.event", {"marker": "another-value"}) original = path.read_text(encoding="utf-8") assert "honest-value" in original path.write_text(original.replace("honest-value", "forged-value"), encoding="utf-8") tampered_detected = False try: verdict = ledger_verify(open_ledger(path)) if verdict is False: tampered_detected = True except Exception: tampered_detected = True assert tampered_detected, "rewriting history must break verification" def test_truncation_is_detected_or_visible(tmp_path): path, lgr = _fresh_ledger(tmp_path) for index in range(4): ledger_append(lgr, "test.event", {"index": index}) lines = path.read_text(encoding="utf-8").splitlines() # Drop an entry from the *middle* of the chain — not the tail — so the # chain linkage must break. if len(lines) >= 3: del lines[1] path.write_text("\n".join(lines) + "\n", encoding="utf-8") broken = False try: verdict = ledger_verify(open_ledger(path)) if verdict is False: broken = True except Exception: broken = True assert broken, "removing a mid-chain entry must break verification" def test_ledger_lines_are_machine_parseable(tmp_path): path, lgr = _fresh_ledger(tmp_path) ledger_append(lgr, "test.event", {"marker": "parse-me"}) raw = path.read_text(encoding="utf-8").strip() assert raw first_line = raw.splitlines()[0] try: parsed = json.loads(first_line) except json.JSONDecodeError: import yaml parsed = yaml.safe_load(raw) assert parsed, "ledger entries must be machine-parseable (JSON lines or YAML)"