146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
"""Тесты журнала аудита «кто/когда/что/old→new» (issue #20)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from awg_api.store import (
|
|
AUDIT_STATUS_FAILED,
|
|
AUDIT_STATUS_OK,
|
|
Database,
|
|
Entity,
|
|
Mode,
|
|
Rule,
|
|
Store,
|
|
)
|
|
from awg_api.store import audit
|
|
|
|
|
|
def make_db(tmp_path: Path) -> Database:
|
|
return Database(tmp_path / "data" / "awg-dash.db")
|
|
|
|
|
|
def test_record_and_read_back(tmp_path: Path) -> None:
|
|
with make_db(tmp_path) as db:
|
|
with db.transaction() as conn:
|
|
entry_id = audit.record(
|
|
conn,
|
|
actor="ui:admin",
|
|
action="update",
|
|
entity=Entity.RULE.value,
|
|
entity_id=7,
|
|
old_value={"match": "old.example.com"},
|
|
new_value={"match": "new.example.com"},
|
|
)
|
|
assert entry_id > 0
|
|
entries = audit.list_entries(db.connection)
|
|
assert len(entries) == 1
|
|
entry = entries[0]
|
|
assert entry.actor == "ui:admin"
|
|
assert entry.entity == "rule"
|
|
assert entry.entity_id == "7"
|
|
assert entry.old_value == {"match": "old.example.com"}
|
|
assert entry.new_value == {"match": "new.example.com"}
|
|
assert entry.status == AUDIT_STATUS_OK
|
|
assert entry.ts.endswith("Z")
|
|
assert entry.as_dict()["entity_id"] == "7"
|
|
|
|
|
|
def test_actor_is_required(tmp_path: Path) -> None:
|
|
with make_db(tmp_path) as db:
|
|
with db.transaction() as conn, pytest.raises(ValueError, match="actor"):
|
|
audit.record(
|
|
conn,
|
|
actor="",
|
|
action="update",
|
|
entity=Entity.RULE.value,
|
|
new_value={"x": 1},
|
|
)
|
|
|
|
|
|
def test_old_and_new_can_be_absent(tmp_path: Path) -> None:
|
|
with make_db(tmp_path) as db:
|
|
with db.transaction() as conn:
|
|
audit.record(conn, actor="system", action="noop", entity=Entity.APPLY.value)
|
|
entry = audit.last_entry(db.connection)
|
|
assert entry is not None
|
|
assert entry.old_value is None
|
|
assert entry.new_value is None
|
|
assert entry.entity_id is None
|
|
assert entry.detail is None
|
|
|
|
|
|
def test_filters_limit_and_ordering(tmp_path: Path) -> None:
|
|
with make_db(tmp_path) as db:
|
|
store = Store(db)
|
|
store.set_mode(Mode.SPLIT, actor="ui:admin")
|
|
store.set_mode(Mode.FULL, actor="ui:admin")
|
|
store.upsert_rule(Rule(name="yt", kind="domain", match="youtube.com", target="tun0"),
|
|
actor="ui:admin")
|
|
|
|
mode_entries = audit.list_entries(db.connection, entity=Entity.MODE.value)
|
|
assert len(mode_entries) == 2
|
|
assert mode_entries[0].new_value == {"mode": "full"} # свежие первыми
|
|
assert mode_entries[0].old_value == {"mode": "split"}
|
|
|
|
by_id = audit.list_entries(
|
|
db.connection, entity=Entity.MODE.value, entity_id=1, limit=1
|
|
)
|
|
assert len(by_id) == 1
|
|
|
|
assert audit.count(db.connection) == 3
|
|
assert audit.count(db.connection, entity=Entity.RULE.value) == 1
|
|
assert audit.last_entry(db.connection, entity=Entity.RULE.value) is not None
|
|
|
|
|
|
def test_ts_can_be_overridden(tmp_path: Path) -> None:
|
|
with make_db(tmp_path) as db:
|
|
with db.transaction() as conn:
|
|
audit.record(
|
|
conn,
|
|
actor="system",
|
|
action="stage",
|
|
entity=Entity.APPLY.value,
|
|
ts="2026-01-02T03:04:05.678Z",
|
|
)
|
|
entry = audit.last_entry(db.connection)
|
|
assert entry is not None and entry.ts == "2026-01-02T03:04:05.678Z"
|
|
|
|
|
|
def test_scalar_and_list_payloads_roundtrip(tmp_path: Path) -> None:
|
|
with make_db(tmp_path) as db:
|
|
with db.transaction() as conn:
|
|
audit.record(
|
|
conn,
|
|
actor="system",
|
|
action="apply",
|
|
entity=Entity.APPLY.value,
|
|
old_value=["a", "b"],
|
|
new_value="plain",
|
|
status=AUDIT_STATUS_FAILED,
|
|
detail="iptables: exit 4",
|
|
)
|
|
entry = audit.last_entry(db.connection)
|
|
assert entry is not None
|
|
assert entry.old_value == ["a", "b"]
|
|
assert entry.new_value == "plain"
|
|
assert entry.status == AUDIT_STATUS_FAILED
|
|
assert entry.detail == "iptables: exit 4"
|
|
|
|
|
|
def test_no_audit_row_when_transaction_rolls_back(tmp_path: Path) -> None:
|
|
"""Мутация и её аудит коммитятся вместе: откат транзакции не оставляет следа."""
|
|
with make_db(tmp_path) as db:
|
|
with pytest.raises(RuntimeError), db.transaction() as conn:
|
|
audit.record(conn, actor="system", action="update", entity=Entity.RULE.value)
|
|
raise RuntimeError("мутация не удалась")
|
|
assert audit.count(db.connection) == 0
|
|
|
|
|
|
def test_last_entry_returns_none_on_empty_log(tmp_path: Path) -> None:
|
|
with make_db(tmp_path) as db:
|
|
assert audit.last_entry(db.connection) is None
|
|
assert audit.list_entries(db.connection) == []
|