Add missing jails router tests to achieve 100% line coverage
All error-handling branches in app/routers/jails.py were previously untested: every Fail2BanConnectionError (502) path, several JailNotFoundError (404) and JailOperationError (409) paths, and the toggle_ignore_self endpoint which had zero coverage. Added 26 new test cases across three new test classes (TestIgnoreIpEndpoints extended, TestToggleIgnoreSelf, TestFail2BanConnectionErrors) covering every remaining branch. - app/routers/jails.py: 61% → 100% line coverage - Overall backend coverage: 83% → 85% - Total test count: 497 → 523 (all pass) - ruff check and mypy --strict clean
This commit is contained in:
@@ -407,3 +407,384 @@ class TestIgnoreIpEndpoints:
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_get_ignore_list_404_for_unknown_jail(self, jails_client: AsyncClient) -> None:
|
||||
"""GET /api/jails/ghost/ignoreip returns 404 for unknown jail."""
|
||||
from app.services.jail_service import JailNotFoundError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.get_ignore_list",
|
||||
AsyncMock(side_effect=JailNotFoundError("ghost")),
|
||||
):
|
||||
resp = await jails_client.get("/api/jails/ghost/ignoreip")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_get_ignore_list_502_on_connection_error(self, jails_client: AsyncClient) -> None:
|
||||
"""GET /api/jails/sshd/ignoreip returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.get_ignore_list",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.get("/api/jails/sshd/ignoreip")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_add_ignore_ip_404_for_unknown_jail(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/ghost/ignoreip returns 404 for unknown jail."""
|
||||
from app.services.jail_service import JailNotFoundError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.add_ignore_ip",
|
||||
AsyncMock(side_effect=JailNotFoundError("ghost")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/ghost/ignoreip",
|
||||
json={"ip": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_add_ignore_ip_409_on_operation_error(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/ignoreip returns 409 on operation failure."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.add_ignore_ip",
|
||||
AsyncMock(side_effect=JailOperationError("fail2ban rejected")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/sshd/ignoreip",
|
||||
json={"ip": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
async def test_add_ignore_ip_502_on_connection_error(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/ignoreip returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.add_ignore_ip",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/sshd/ignoreip",
|
||||
json={"ip": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_delete_ignore_ip_404_for_unknown_jail(self, jails_client: AsyncClient) -> None:
|
||||
"""DELETE /api/jails/ghost/ignoreip returns 404 for unknown jail."""
|
||||
from app.services.jail_service import JailNotFoundError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.del_ignore_ip",
|
||||
AsyncMock(side_effect=JailNotFoundError("ghost")),
|
||||
):
|
||||
resp = await jails_client.request(
|
||||
"DELETE",
|
||||
"/api/jails/ghost/ignoreip",
|
||||
json={"ip": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_delete_ignore_ip_409_on_operation_error(self, jails_client: AsyncClient) -> None:
|
||||
"""DELETE /api/jails/sshd/ignoreip returns 409 on operation failure."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.del_ignore_ip",
|
||||
AsyncMock(side_effect=JailOperationError("fail2ban rejected")),
|
||||
):
|
||||
resp = await jails_client.request(
|
||||
"DELETE",
|
||||
"/api/jails/sshd/ignoreip",
|
||||
json={"ip": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
async def test_delete_ignore_ip_502_on_connection_error(self, jails_client: AsyncClient) -> None:
|
||||
"""DELETE /api/jails/sshd/ignoreip returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.del_ignore_ip",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.request(
|
||||
"DELETE",
|
||||
"/api/jails/sshd/ignoreip",
|
||||
json={"ip": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /api/jails/{name}/ignoreself
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestToggleIgnoreSelf:
|
||||
"""Tests for ``POST /api/jails/{name}/ignoreself``."""
|
||||
|
||||
async def test_200_enables_ignore_self(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/ignoreself with ``true`` returns 200."""
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.set_ignore_self",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/sshd/ignoreself",
|
||||
content="true",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "enabled" in resp.json()["message"]
|
||||
|
||||
async def test_200_disables_ignore_self(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/ignoreself with ``false`` returns 200."""
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.set_ignore_self",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/sshd/ignoreself",
|
||||
content="false",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "disabled" in resp.json()["message"]
|
||||
|
||||
async def test_404_for_unknown_jail(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/ghost/ignoreself returns 404 for unknown jail."""
|
||||
from app.services.jail_service import JailNotFoundError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.set_ignore_self",
|
||||
AsyncMock(side_effect=JailNotFoundError("ghost")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/ghost/ignoreself",
|
||||
content="true",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_409_on_operation_error(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/ignoreself returns 409 on operation failure."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.set_ignore_self",
|
||||
AsyncMock(side_effect=JailOperationError("fail2ban rejected")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/sshd/ignoreself",
|
||||
content="true",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
async def test_502_on_connection_error(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/ignoreself returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.set_ignore_self",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/sshd/ignoreself",
|
||||
content="true",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 502 error paths — Fail2BanConnectionError across remaining endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFail2BanConnectionErrors:
|
||||
"""Tests that every endpoint returns 502 when fail2ban is unreachable."""
|
||||
|
||||
async def test_get_jails_502(self, jails_client: AsyncClient) -> None:
|
||||
"""GET /api/jails returns 502 when fail2ban socket is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.list_jails",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.get("/api/jails")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_get_jail_502(self, jails_client: AsyncClient) -> None:
|
||||
"""GET /api/jails/sshd returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.get_jail",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.get("/api/jails/sshd")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_reload_all_409(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/reload-all returns 409 on operation failure."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.reload_all",
|
||||
AsyncMock(side_effect=JailOperationError("reload failed")),
|
||||
):
|
||||
resp = await jails_client.post("/api/jails/reload-all")
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
async def test_reload_all_502(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/reload-all returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.reload_all",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.post("/api/jails/reload-all")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_start_jail_502(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/start returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.start_jail",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.post("/api/jails/sshd/start")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_stop_jail_409(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/stop returns 409 on operation failure."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.stop_jail",
|
||||
AsyncMock(side_effect=JailOperationError("stop failed")),
|
||||
):
|
||||
resp = await jails_client.post("/api/jails/sshd/stop")
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
async def test_stop_jail_502(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/stop returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.stop_jail",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.post("/api/jails/sshd/stop")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_toggle_idle_404(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/ghost/idle returns 404 for unknown jail."""
|
||||
from app.services.jail_service import JailNotFoundError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.set_idle",
|
||||
AsyncMock(side_effect=JailNotFoundError("ghost")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/ghost/idle",
|
||||
content="true",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_toggle_idle_409(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/idle returns 409 on operation failure."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.set_idle",
|
||||
AsyncMock(side_effect=JailOperationError("idle failed")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/sshd/idle",
|
||||
content="true",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
async def test_toggle_idle_502(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/idle returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.set_idle",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.post(
|
||||
"/api/jails/sshd/idle",
|
||||
content="true",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_reload_jail_404(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/ghost/reload returns 404 for unknown jail."""
|
||||
from app.services.jail_service import JailNotFoundError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.reload_jail",
|
||||
AsyncMock(side_effect=JailNotFoundError("ghost")),
|
||||
):
|
||||
resp = await jails_client.post("/api/jails/ghost/reload")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_reload_jail_409(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/reload returns 409 on operation failure."""
|
||||
from app.services.jail_service import JailOperationError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.reload_jail",
|
||||
AsyncMock(side_effect=JailOperationError("reload failed")),
|
||||
):
|
||||
resp = await jails_client.post("/api/jails/sshd/reload")
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
async def test_reload_jail_502(self, jails_client: AsyncClient) -> None:
|
||||
"""POST /api/jails/sshd/reload returns 502 when fail2ban is unreachable."""
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
with patch(
|
||||
"app.routers.jails.jail_service.reload_jail",
|
||||
AsyncMock(side_effect=Fail2BanConnectionError("socket dead", "/tmp/fake.sock")),
|
||||
):
|
||||
resp = await jails_client.post("/api/jails/sshd/reload")
|
||||
|
||||
assert resp.status_code == 502
|
||||
|
||||
Reference in New Issue
Block a user