fix: retry, semaphore, reload lock, activation verify, bans_by_jail diagnostics
Stage 1.1-1.3: reload_all include/exclude_jails params already implemented; added keyword-arg assertions in router and service tests. Stage 2.1/6.1: _send_command_sync retry loop (3 attempts, 150ms exp backoff) retrying on EAGAIN/ECONNREFUSED/ENOBUFS; immediate raise on all other errors. Stage 2.2: asyncio.Lock at module level in jail_service.reload_all to serialize concurrent reload--all commands. Stage 3.1: activate_jail re-queries _get_active_jail_names after reload; returns active=False with descriptive message if jail did not start. Stage 4.1/6.2: asyncio.Semaphore (max 10) in Fail2BanClient.send, lazy- initialized; logs fail2ban_command_waiting_semaphore at debug when waiting. Stage 5.1/5.2: unit tests asserting reload_all is called with include_jails and exclude_jails; activation verification happy/sad path tests. Stage 6.3: TestSendCommandSyncRetry (5 cases) + TestFail2BanClientSemaphore concurrency test. Stage 7.1-7.3: _since_unix uses time.time(); bans_by_jail debug logging with since_iso; diagnostic warning when total==0 despite table rows; unit test verifying the warning fires for stale data.
This commit is contained in:
@@ -440,7 +440,7 @@ class TestActivateJail:
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
new=AsyncMock(side_effect=[set(), {"apache-auth"}]),
|
||||
),
|
||||
patch("app.services.config_file_service.jail_service") as mock_js,
|
||||
):
|
||||
@@ -2491,3 +2491,112 @@ class TestRemoveActionFromJail:
|
||||
|
||||
mock_reload.assert_awaited_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# activate_jail — reload_all keyword argument assertions (Stage 5.1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestActivateJailReloadArgs:
|
||||
"""Verify activate_jail calls reload_all with include_jails=[name]."""
|
||||
|
||||
async def test_activate_passes_include_jails(self, tmp_path: Path) -> None:
|
||||
"""activate_jail must pass include_jails=[name] to reload_all."""
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
from app.models.config import ActivateJailRequest
|
||||
|
||||
req = ActivateJailRequest()
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(side_effect=[set(), {"apache-auth"}]),
|
||||
),
|
||||
patch("app.services.config_file_service.jail_service") as mock_js,
|
||||
):
|
||||
mock_js.reload_all = AsyncMock()
|
||||
await activate_jail(str(tmp_path), "/fake.sock", "apache-auth", req)
|
||||
|
||||
mock_js.reload_all.assert_awaited_once_with(
|
||||
"/fake.sock", include_jails=["apache-auth"]
|
||||
)
|
||||
|
||||
async def test_activate_returns_active_true_when_jail_starts(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
"""activate_jail returns active=True when the jail appears in post-reload names."""
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
from app.models.config import ActivateJailRequest
|
||||
|
||||
req = ActivateJailRequest()
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(side_effect=[set(), {"apache-auth"}]),
|
||||
),
|
||||
patch("app.services.config_file_service.jail_service") as mock_js,
|
||||
):
|
||||
mock_js.reload_all = AsyncMock()
|
||||
result = await activate_jail(
|
||||
str(tmp_path), "/fake.sock", "apache-auth", req
|
||||
)
|
||||
|
||||
assert result.active is True
|
||||
assert "activated" in result.message.lower()
|
||||
|
||||
async def test_activate_returns_active_false_when_jail_does_not_start(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
"""activate_jail returns active=False when the jail is absent after reload.
|
||||
|
||||
This covers the Stage 3.1 requirement: if the jail config is invalid
|
||||
(bad regex, missing log file, etc.) fail2ban may silently refuse to
|
||||
start the jail even though the reload command succeeded.
|
||||
"""
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
from app.models.config import ActivateJailRequest
|
||||
|
||||
req = ActivateJailRequest()
|
||||
# Pre-reload: jail not running. Post-reload: still not running (boot failed).
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(side_effect=[set(), set()]),
|
||||
),
|
||||
patch("app.services.config_file_service.jail_service") as mock_js,
|
||||
):
|
||||
mock_js.reload_all = AsyncMock()
|
||||
result = await activate_jail(
|
||||
str(tmp_path), "/fake.sock", "apache-auth", req
|
||||
)
|
||||
|
||||
assert result.active is False
|
||||
assert "apache-auth" in result.name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# deactivate_jail — reload_all keyword argument assertions (Stage 5.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDeactivateJailReloadArgs:
|
||||
"""Verify deactivate_jail calls reload_all with exclude_jails=[name]."""
|
||||
|
||||
async def test_deactivate_passes_exclude_jails(self, tmp_path: Path) -> None:
|
||||
"""deactivate_jail must pass exclude_jails=[name] to reload_all."""
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value={"sshd"}),
|
||||
),
|
||||
patch("app.services.config_file_service.jail_service") as mock_js,
|
||||
):
|
||||
mock_js.reload_all = AsyncMock()
|
||||
await deactivate_jail(str(tmp_path), "/fake.sock", "sshd")
|
||||
|
||||
mock_js.reload_all.assert_awaited_once_with(
|
||||
"/fake.sock", exclude_jails=["sshd"]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user