"""Tests for tamper-evident audit logging.""" import json from pathlib import Path import pytest from aegis_gitea_mcp.audit import AuditLogger, validate_audit_log_integrity def test_audit_log_integrity_valid(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Fresh audit log should validate with intact hash chain.""" monkeypatch.setenv("GITEA_URL", "https://gitea.example.com") log_path = tmp_path / "audit.log" logger = AuditLogger(log_path=log_path) logger.log_tool_invocation("list_repositories", result_status="pending") logger.log_tool_invocation("list_repositories", result_status="success") logger.close() valid, errors = validate_audit_log_integrity(log_path) assert valid assert errors == [] def test_audit_log_integrity_detects_tamper( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Integrity validation should fail when entries are modified.""" monkeypatch.setenv("GITEA_URL", "https://gitea.example.com") log_path = tmp_path / "audit.log" logger = AuditLogger(log_path=log_path) logger.log_tool_invocation("list_repositories", result_status="pending") logger.log_tool_invocation("list_repositories", result_status="success") logger.close() lines = log_path.read_text(encoding="utf-8").splitlines() first_entry = json.loads(lines[0]) first_entry["payload"]["tool_name"] = "tampered" lines[0] = json.dumps(first_entry) log_path.write_text("\n".join(lines) + "\n", encoding="utf-8") valid, errors = validate_audit_log_integrity(log_path) assert not valid assert errors def test_audit_logger_uses_configured_path(mock_env: None, tmp_path: Path) -> None: """Without an explicit path the logger writes to AUDIT_LOG_PATH (tmp in tests).""" from aegis_gitea_mcp.audit import get_audit_logger logger = get_audit_logger() logger.log_access_denied("tool", reason="nope") logger.log_security_event("evt", "description", metadata={"k": "v"}) assert logger.log_path == tmp_path / "data" / "audit.log" assert len(logger.log_path.read_text(encoding="utf-8").splitlines()) == 2 valid, errors = validate_audit_log_integrity(logger.log_path) assert valid, errors def test_audit_masks_secrets_and_continues_chain(mock_env: None, tmp_path: Path) -> None: """Secrets are masked before persistence and a reopened log continues the chain.""" log_path = tmp_path / "chain.log" first = AuditLogger(log_path=log_path) first.log_tool_invocation("t", params={"note": "key AKIA1234567890ABCDEF"}) first.close() second = AuditLogger(log_path=log_path) second.log_tool_invocation("t", error="boom", result_status="error") second.close() text = log_path.read_text(encoding="utf-8") assert "AKIA1234567890ABCDEF" not in text valid, errors = validate_audit_log_integrity(log_path) assert valid, errors def test_validate_missing_log_is_valid(tmp_path: Path) -> None: assert validate_audit_log_integrity(tmp_path / "absent.log") == (True, [])