Fix stale activation record on failed jail activation

Record activation only after a successful jail activate request and add regression coverage to prevent stale last_activation state.
This commit is contained in:
2026-04-17 14:53:57 +02:00
parent 73cc212e28
commit 13b3fde274
4 changed files with 33 additions and 12 deletions

View File

@@ -839,6 +839,24 @@ class TestActivateJail:
assert resp.status_code == 409
async def test_failed_activation_does_not_set_last_activation(
self, config_client: AsyncClient
) -> None:
"""A failed activation must not leave a stale last_activation record."""
from app.exceptions import Fail2BanConnectionError
config_client._transport.app.state.last_activation = None
with patch(
"app.routers.jail_config.jail_config_service.activate_jail",
AsyncMock(side_effect=Fail2BanConnectionError("No socket", "/tmp/fake.sock")),
):
resp = await config_client.post(
"/api/config/jails/sshd/activate", json={}
)
assert resp.status_code == 502
assert config_client._transport.app.state.last_activation is None
async def test_400_for_invalid_jail_name(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/ with bad name returns 400."""
from app.exceptions import JailNameError
@@ -1903,7 +1921,7 @@ class TestGetFail2BanLog:
async def test_200_returns_log_response(self, config_client: AsyncClient) -> None:
"""GET /api/config/fail2ban-log returns 200 with Fail2BanLogResponse."""
with patch(
"app.routers.config_misc.config_service.read_fail2ban_log",
"app.routers.config_misc.log_service.read_fail2ban_log",
AsyncMock(return_value=self._mock_log_response()),
):
resp = await config_client.get("/api/config/fail2ban-log")
@@ -1918,7 +1936,7 @@ class TestGetFail2BanLog:
async def test_200_passes_lines_query_param(self, config_client: AsyncClient) -> None:
"""GET /api/config/fail2ban-log passes the lines query param to the service."""
with patch(
"app.routers.config_misc.config_service.read_fail2ban_log",
"app.routers.config_misc.log_service.read_fail2ban_log",
AsyncMock(return_value=self._mock_log_response()),
) as mock_fn:
resp = await config_client.get("/api/config/fail2ban-log?lines=500")
@@ -1930,7 +1948,7 @@ class TestGetFail2BanLog:
async def test_200_passes_filter_query_param(self, config_client: AsyncClient) -> None:
"""GET /api/config/fail2ban-log passes the filter query param to the service."""
with patch(
"app.routers.config_misc.config_service.read_fail2ban_log",
"app.routers.config_misc.log_service.read_fail2ban_log",
AsyncMock(return_value=self._mock_log_response()),
) as mock_fn:
resp = await config_client.get("/api/config/fail2ban-log?filter=ERROR")
@@ -1944,7 +1962,7 @@ class TestGetFail2BanLog:
from app.services.config_service import ConfigOperationError
with patch(
"app.routers.config_misc.config_service.read_fail2ban_log",
"app.routers.config_misc.log_service.read_fail2ban_log",
AsyncMock(side_effect=ConfigOperationError("fail2ban is logging to 'STDOUT'")),
):
resp = await config_client.get("/api/config/fail2ban-log")
@@ -1956,7 +1974,7 @@ class TestGetFail2BanLog:
from app.services.config_service import ConfigOperationError
with patch(
"app.routers.config_misc.config_service.read_fail2ban_log",
"app.routers.config_misc.log_service.read_fail2ban_log",
AsyncMock(side_effect=ConfigOperationError("outside the allowed directory")),
):
resp = await config_client.get("/api/config/fail2ban-log")
@@ -1968,7 +1986,7 @@ class TestGetFail2BanLog:
from app.exceptions import Fail2BanConnectionError
with patch(
"app.routers.config_misc.config_service.read_fail2ban_log",
"app.routers.config_misc.log_service.read_fail2ban_log",
AsyncMock(side_effect=Fail2BanConnectionError("socket error", "/tmp/f.sock")),
):
resp = await config_client.get("/api/config/fail2ban-log")
@@ -2011,7 +2029,7 @@ class TestGetServiceStatus:
async def test_200_when_online(self, config_client: AsyncClient) -> None:
"""GET /api/config/service-status returns 200 with full status when online."""
with patch(
"app.routers.config_misc.config_service.get_service_status",
"app.routers.config_misc.health_service.get_service_status",
AsyncMock(return_value=self._mock_status(online=True)),
):
resp = await config_client.get("/api/config/service-status")
@@ -2026,7 +2044,7 @@ class TestGetServiceStatus:
async def test_200_when_offline(self, config_client: AsyncClient) -> None:
"""GET /api/config/service-status returns 200 with offline=False when daemon is down."""
with patch(
"app.routers.config_misc.config_service.get_service_status",
"app.routers.config_misc.health_service.get_service_status",
AsyncMock(return_value=self._mock_status(online=False)),
):
resp = await config_client.get("/api/config/service-status")