"""Tests for app.utils.fail2ban_client.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from app.utils.fail2ban_client import ( Fail2BanClient, Fail2BanConnectionError, Fail2BanProtocolError, _send_command_sync, ) class TestFail2BanClientPing: """Tests for :meth:`Fail2BanClient.ping`.""" @pytest.mark.asyncio async def test_ping_returns_true_when_daemon_responds(self) -> None: """``ping()`` must return ``True`` when fail2ban responds with 1.""" client = Fail2BanClient(socket_path="/fake/fail2ban.sock") with patch.object(client, "send", new_callable=AsyncMock, return_value=1): result = await client.ping() assert result is True @pytest.mark.asyncio async def test_ping_returns_false_on_connection_error(self) -> None: """``ping()`` must return ``False`` when the daemon is unreachable.""" client = Fail2BanClient(socket_path="/fake/fail2ban.sock") with patch.object( client, "send", new_callable=AsyncMock, side_effect=Fail2BanConnectionError("refused", "/fake/fail2ban.sock"), ): result = await client.ping() assert result is False @pytest.mark.asyncio async def test_ping_returns_false_on_protocol_error(self) -> None: """``ping()`` must return ``False`` if the response cannot be parsed.""" client = Fail2BanClient(socket_path="/fake/fail2ban.sock") with patch.object( client, "send", new_callable=AsyncMock, side_effect=Fail2BanProtocolError("bad pickle"), ): result = await client.ping() assert result is False class TestFail2BanClientContextManager: """Tests for the async context manager protocol.""" @pytest.mark.asyncio async def test_context_manager_returns_self(self) -> None: """``async with Fail2BanClient(...)`` must yield the client itself.""" client = Fail2BanClient(socket_path="/fake/fail2ban.sock") async with client as ctx: assert ctx is client class TestSendCommandSync: """Tests for the synchronous :func:`_send_command_sync` helper.""" def test_send_command_sync_raises_connection_error_when_socket_absent(self) -> None: """Must raise :class:`Fail2BanConnectionError` if the socket does not exist.""" with pytest.raises(Fail2BanConnectionError): _send_command_sync( socket_path="/nonexistent/fail2ban.sock", command=["ping"], timeout=1.0, ) def test_send_command_sync_raises_connection_error_on_oserror(self) -> None: """Must translate :class:`OSError` into :class:`Fail2BanConnectionError`.""" with patch("socket.socket") as mock_socket_cls: mock_sock = MagicMock() mock_sock.connect.side_effect = OSError("connection refused") mock_socket_cls.return_value = mock_sock with pytest.raises(Fail2BanConnectionError): _send_command_sync( socket_path="/fake/fail2ban.sock", command=["status"], timeout=1.0, )