Adds the pydantic-settings Settings with validated GITEA_URL, OAuth scopes and toolsets, the argparse CLI skeleton, catalog.model, identity and GiteaAPIError. Test fixtures now isolate env, cwd and state paths in tmp.
81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
"""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, [])
|