"""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")