Files
Orchestrator/shared/tools/monorepo-cli/tests/test_audit.py
T
2026-06-30 20:37:40 +02:00

171 lines
6.0 KiB
Python

"""Tests für den AuditLogger."""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import pytest
from monorepo.audit import AuditLogger, _DEFAULT_AUDIT_LOG_PATH
from monorepo.models import SecurityEvent
class TestAuditLoggerInit:
"""Tests für die Initialisierung des AuditLoggers."""
def test_default_log_path(self, tmp_path: Path) -> None:
"""AuditLogger mit Standard-Pfad nutzt _DEFAULT_AUDIT_LOG_PATH."""
# Wir übergeben explizit einen tmp-Pfad, da der Default relativ ist
log_path = tmp_path / ".audit" / "access.log"
logger = AuditLogger(log_path=log_path)
assert logger.log_path == log_path
def test_creates_log_directory(self, tmp_path: Path) -> None:
"""AuditLogger erstellt das Verzeichnis, falls es nicht existiert."""
log_path = tmp_path / "nested" / "deep" / "audit.log"
assert not log_path.parent.exists()
AuditLogger(log_path=log_path)
assert log_path.parent.exists()
def test_existing_directory_no_error(self, tmp_path: Path) -> None:
"""AuditLogger wirft keinen Fehler bei bereits existierendem Verzeichnis."""
log_dir = tmp_path / ".audit"
log_dir.mkdir()
log_path = log_dir / "access.log"
logger = AuditLogger(log_path=log_path)
assert logger.log_path == log_path
class TestLogViolation:
"""Tests für die log_violation-Methode."""
def _make_event(
self,
requesting: str = "dhive",
target: str = "privat",
resource: str = "privat/.env",
action: str = "read",
outcome: str = "denied",
ts: datetime | None = None,
) -> SecurityEvent:
"""Erstellt ein Test-SecurityEvent."""
return SecurityEvent(
timestamp=ts or datetime(2025, 1, 15, 10, 30, 0),
requesting_context=requesting,
target_context=target,
resource=resource,
action=action,
outcome=outcome,
)
def test_writes_single_entry(self, tmp_path: Path) -> None:
"""log_violation schreibt einen Eintrag in die Logdatei."""
log_path = tmp_path / ".audit" / "access.log"
logger = AuditLogger(log_path=log_path)
event = self._make_event()
logger.log_violation(event)
content = log_path.read_text(encoding="utf-8")
assert "[2025-01-15T10:30:00] DENIED | dhive -> privat | read on privat/.env\n" == content
def test_appends_multiple_entries(self, tmp_path: Path) -> None:
"""log_violation fügt Einträge an, ohne bestehende zu überschreiben."""
log_path = tmp_path / ".audit" / "access.log"
logger = AuditLogger(log_path=log_path)
event1 = self._make_event(
requesting="bahn", target="privat", resource="privat/.env",
action="read", outcome="denied",
ts=datetime(2025, 1, 15, 10, 0, 0),
)
event2 = self._make_event(
requesting="dhive", target="bahn", resource="bahn/.env",
action="write", outcome="denied",
ts=datetime(2025, 1, 15, 11, 0, 0),
)
logger.log_violation(event1)
logger.log_violation(event2)
lines = log_path.read_text(encoding="utf-8").splitlines()
assert len(lines) == 2
assert "bahn -> privat" in lines[0]
assert "dhive -> bahn" in lines[1]
def test_format_contains_iso_timestamp(self, tmp_path: Path) -> None:
"""Der Zeitstempel ist im ISO-8601-Format."""
log_path = tmp_path / ".audit" / "access.log"
logger = AuditLogger(log_path=log_path)
ts = datetime(2025, 6, 20, 14, 45, 30)
event = self._make_event(ts=ts)
logger.log_violation(event)
content = log_path.read_text(encoding="utf-8")
assert "[2025-06-20T14:45:30]" in content
def test_format_outcome_uppercase(self, tmp_path: Path) -> None:
"""Das Outcome wird in Großbuchstaben geschrieben."""
log_path = tmp_path / ".audit" / "access.log"
logger = AuditLogger(log_path=log_path)
event = self._make_event(outcome="allowed")
logger.log_violation(event)
content = log_path.read_text(encoding="utf-8")
assert "ALLOWED" in content
def test_format_contains_action_and_resource(self, tmp_path: Path) -> None:
"""Die Aktion und Ressource werden korrekt formatiert."""
log_path = tmp_path / ".audit" / "access.log"
logger = AuditLogger(log_path=log_path)
event = self._make_event(action="execute", resource="shared/tools/script.sh")
logger.log_violation(event)
content = log_path.read_text(encoding="utf-8")
assert "execute on shared/tools/script.sh" in content
def test_thread_safety(self, tmp_path: Path) -> None:
"""Parallele Aufrufe erzeugen keine korrupten Einträge."""
import threading
log_path = tmp_path / ".audit" / "access.log"
logger = AuditLogger(log_path=log_path)
events = [
self._make_event(
requesting=f"ctx{i}",
target="privat",
resource="privat/.env",
ts=datetime(2025, 1, 15, 10, i, 0),
)
for i in range(20)
]
threads = [
threading.Thread(target=logger.log_violation, args=(e,))
for e in events
]
for t in threads:
t.start()
for t in threads:
t.join()
lines = log_path.read_text(encoding="utf-8").splitlines()
assert len(lines) == 20
# Jede Zeile muss vollständig sein (beginnt mit [ und endet mit resource)
for line in lines:
assert line.startswith("[")
assert "privat/.env" in line
class TestDefaultPath:
"""Tests für den Standard-Audit-Log-Pfad."""
def test_default_path_value(self) -> None:
"""Der Standard-Pfad entspricht der monorepo.yaml-Konfiguration."""
assert _DEFAULT_AUDIT_LOG_PATH == Path(".audit/access.log")