Fix restart/reload endpoint correctness and safety
- jail_service.restart(): replace invalid ["restart"] socket command with ["stop"], matching fail2ban transmitter protocol. The daemon is now stopped via socket; the caller starts it via subprocess. - config_file_service: expose _start_daemon and _wait_for_fail2ban as public start_daemon / wait_for_fail2ban functions. - restart_fail2ban router: orchestrate stop (socket) → start (subprocess) → probe (socket). Returns 204 on success, 503 when fail2ban does not come back within 10 s. Catches JailOperationError → 409. - reload_fail2ban router: add JailOperationError catch → 409 Conflict, consistent with other jail control endpoints. - Tests: add TestJailControls.test_restart_* (3 cases), TestReloadFail2ban 502/409 cases, TestRestartFail2ban (5 cases), TestRollbackJail (6 integration tests verifying file-write, subprocess invocation, socket- probe truthiness, active_jails count, and offline-at-call-time).
This commit is contained in:
@@ -370,6 +370,124 @@ class TestReloadFail2ban:
|
||||
|
||||
assert resp.status_code == 204
|
||||
|
||||
async def test_502_when_fail2ban_unreachable(self, config_client: AsyncClient) -> None:
|
||||
"""POST /api/config/reload returns 502 when fail2ban socket is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.config.jail_service.reload_all",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("no socket", "/fake.sock")),
|
||||
):
|
||||
resp = await config_client.post("/api/config/reload")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_409_when_reload_operation_fails(self, config_client: AsyncClient) -> None:
|
||||
"""POST /api/config/reload returns 409 when fail2ban reports a reload error."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.config.jail_service.reload_all",
|
||||
AsyncMock(side_effect=JailOperationError("reload rejected")),
|
||||
):
|
||||
resp = await config_client.post("/api/config/reload")
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /api/config/restart
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRestartFail2ban:
|
||||
"""Tests for ``POST /api/config/restart``."""
|
||||
|
||||
async def test_204_on_success(self, config_client: AsyncClient) -> None:
|
||||
"""POST /api/config/restart returns 204 when fail2ban restarts cleanly."""
|
||||
with (
|
||||
patch(
|
||||
"app.routers.config.jail_service.restart",
|
||||
AsyncMock(return_value=None),
|
||||
),
|
||||
patch(
|
||||
"app.routers.config.config_file_service.start_daemon",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"app.routers.config.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
):
|
||||
resp = await config_client.post("/api/config/restart")
|
||||
|
||||
assert resp.status_code == 204
|
||||
|
||||
async def test_503_when_fail2ban_does_not_come_back(self, config_client: AsyncClient) -> None:
|
||||
"""POST /api/config/restart returns 503 when fail2ban does not come back online."""
|
||||
with (
|
||||
patch(
|
||||
"app.routers.config.jail_service.restart",
|
||||
AsyncMock(return_value=None),
|
||||
),
|
||||
patch(
|
||||
"app.routers.config.config_file_service.start_daemon",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"app.routers.config.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=False),
|
||||
),
|
||||
):
|
||||
resp = await config_client.post("/api/config/restart")
|
||||
|
||||
assert resp.status_code == 503
|
||||
|
||||
async def test_409_when_stop_command_fails(self, config_client: AsyncClient) -> None:
|
||||
"""POST /api/config/restart returns 409 when fail2ban rejects the stop command."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.config.jail_service.restart",
|
||||
AsyncMock(side_effect=JailOperationError("stop failed")),
|
||||
):
|
||||
resp = await config_client.post("/api/config/restart")
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
async def test_502_when_fail2ban_unreachable(self, config_client: AsyncClient) -> None:
|
||||
"""POST /api/config/restart returns 502 when fail2ban socket is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.config.jail_service.restart",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("no socket", "/fake.sock")),
|
||||
):
|
||||
resp = await config_client.post("/api/config/restart")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_start_daemon_called_after_stop(self, config_client: AsyncClient) -> None:
|
||||
"""start_daemon is called after a successful stop."""
|
||||
mock_start = AsyncMock(return_value=True)
|
||||
with (
|
||||
patch(
|
||||
"app.routers.config.jail_service.restart",
|
||||
AsyncMock(return_value=None),
|
||||
),
|
||||
patch(
|
||||
"app.routers.config.config_file_service.start_daemon",
|
||||
mock_start,
|
||||
),
|
||||
patch(
|
||||
"app.routers.config.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
):
|
||||
await config_client.post("/api/config/restart")
|
||||
|
||||
mock_start.assert_awaited_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /api/config/regex-test
|
||||
|
||||
Reference in New Issue
Block a user