"""The audit log is fail closed after startup too (SEC-03) and names the client (SEC-12).""" from __future__ import annotations import json from typing import Any import pytest from pytest_httpx import HTTPXMock from aegis_gitea_mcp.audit import get_audit_logger from tests.test_server_helpers import API, connected, mock_user REPO_ARGS = {"owner": "octo", "repo": "demo"} @pytest.fixture(autouse=True) def _env(mock_env: None) -> None: """The audit logger reads the process-wide settings.""" class FailingFile: """Stands in for the open audit file and fails every write like a full disk.""" def __init__(self) -> None: self.attempts = 0 def write(self, data: str) -> int: self.attempts += 1 raise OSError(28, "No space left on device") def flush(self) -> None: # pragma: no cover - never reached raise OSError(28, "No space left on device") def close(self) -> None: return None def payload_of(result: Any) -> dict[str, Any]: decoded: dict[str, Any] = json.loads(result.content[0].text) return decoded async def test_calls_stop_when_the_audit_log_stops_accepting_writes( httpx_mock: HTTPXMock, ) -> None: mock_user(httpx_mock) httpx_mock.add_response(url=f"{API}/repos/octo/demo", json={"name": "demo"}, is_reusable=True) async with connected() as client: first = await client.call_tool("gitea_repo_get", REPO_ARGS) assert not first.is_error sent_before = len(httpx_mock.get_requests()) audit = get_audit_logger() healthy_file = audit._log_file audit._log_file = FailingFile() # type: ignore[assignment] second = await client.call_tool("gitea_repo_get", REPO_ARGS) mutation = await client.call_tool( "gitea_issue_create_issue", {**REPO_ARGS, "body": {"title": "x"}} ) assert second.is_error and mutation.is_error assert payload_of(second)["error"]["kind"] == "audit_unavailable" assert payload_of(mutation)["error"]["kind"] == "audit_unavailable" # Gitea was not contacted at all while the log was broken. assert len(httpx_mock.get_requests()) == sent_before audit._log_file = healthy_file resumed = await client.call_tool("gitea_repo_get", REPO_ARGS) assert not resumed.is_error async def test_the_refusal_does_not_leak_the_failure_reason() -> None: async with connected() as client: get_audit_logger()._log_file = FailingFile() # type: ignore[assignment] result = await client.call_tool("gitea_repo_get", REPO_ARGS) text = result.content[0].text # type: ignore[union-attr] assert "No space left" not in text and "OSError" not in text