Fix undefined names and config router imports / task status update

This commit is contained in:
2026-04-14 13:53:39 +02:00
parent 09c764cebc
commit 6b436dc354
6 changed files with 573 additions and 167 deletions

View File

@@ -51,7 +51,7 @@ def _not_found(name: str) -> HTTPException:
)
@router.get(
"/",
"",
response_model=ActionListResponse,
summary="List all available actions with active/inactive status",
)
@@ -179,7 +179,7 @@ async def update_action(
@router.post(
"/",
"",
response_model=ActionConfig,
status_code=status.HTTP_201_CREATED,
summary="Create a new user-defined action",

View File

@@ -2,9 +2,11 @@ from __future__ import annotations
from typing import Annotated
import structlog
from fastapi import APIRouter, HTTPException, Query, Request, status
from app.dependencies import AuthDep, DbDep, Fail2BanSocketDep, Fail2BanStartCommandDep
from app.exceptions import ConfigOperationError, JailOperationError
from app.models.config import (
Fail2BanLogResponse,
GlobalConfigResponse,
@@ -20,6 +22,8 @@ from app.models.config import (
from app.services import config_file_service, config_service, jail_service, log_service, setup_service
from app.utils.fail2ban_client import Fail2BanConnectionError
log: structlog.stdlib.BoundLogger = structlog.get_logger()
router: APIRouter = APIRouter(tags=["Config Misc"])
@@ -29,6 +33,13 @@ def _bad_gateway(exc: Exception) -> HTTPException:
detail=f"Cannot reach fail2ban: {exc}",
)
def _bad_request(message: str) -> HTTPException:
return HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=message,
)
@router.get(
"/global",
response_model=GlobalConfigResponse,
@@ -286,8 +297,6 @@ async def get_map_color_thresholds(
:class:`~app.models.config.MapColorThresholdsResponse` with
current thresholds.
"""
from app.services import setup_service
high, medium, low = await setup_service.get_map_color_thresholds(db)
return MapColorThresholdsResponse(
threshold_high=high,
@@ -298,6 +307,7 @@ async def get_map_color_thresholds(
@router.put(
"/map-color-thresholds",
response_model=MapColorThresholdsResponse,
@@ -324,8 +334,6 @@ async def update_map_color_thresholds(
HTTPException: 400 if validation fails (thresholds not
properly ordered).
"""
from app.services import setup_service
try:
await setup_service.set_map_color_thresholds(
db,
@@ -382,7 +390,7 @@ async def get_fail2ban_log(
"""
try:
return await config_service.read_fail2ban_log(socket_path, lines, filter)
except config_service.ConfigOperationError as exc:
except ConfigOperationError as exc:
raise _bad_request(str(exc)) from exc
except Fail2BanConnectionError as exc:
raise _bad_gateway(exc) from exc

View File

@@ -56,7 +56,7 @@ def _not_found(name: str) -> HTTPException:
)
@router.get(
"/",
"",
response_model=FilterListResponse,
summary="List all available filters with active/inactive status",
)
@@ -207,7 +207,7 @@ async def update_filter(
@router.post(
"/",
"",
response_model=FilterConfig,
status_code=status.HTTP_201_CREATED,
summary="Create a new user-defined filter",

View File

@@ -95,7 +95,7 @@ def _action_not_found(name: str) -> HTTPException:
)
@router.get(
"/",
"",
response_model=JailConfigListResponse,
summary="List configuration for all active jails",
)
@@ -151,6 +151,29 @@ async def get_inactive_jails(
return await jail_config_service.list_inactive_jails(config_dir, socket_path)
@router.get(
"/pending-recovery",
response_model=PendingRecovery | None,
summary="Return active crash-recovery record if one exists",
)
async def get_pending_recovery(
_auth: AuthDep,
pending_recovery: PendingRecoveryDep,
) -> PendingRecovery | None:
"""Return the current :class:`~app.models.config.PendingRecovery` record.
A non-null response means fail2ban crashed shortly after a jail activation
and the user should be offered a rollback option. Returns ``null`` (HTTP
200 with ``null`` body) when no recovery is pending.
Args:
request: FastAPI request object.
_auth: Validated session.
Returns:
:class:`~app.models.config.PendingRecovery` or ``None``.
"""
return pending_recovery
@router.get(
@@ -526,35 +549,6 @@ async def validate_jail(
raise _bad_request(str(exc)) from exc
@router.get(
"/pending-recovery",
response_model=PendingRecovery | None,
summary="Return active crash-recovery record if one exists",
)
async def get_pending_recovery(
_auth: AuthDep,
pending_recovery: PendingRecoveryDep,
) -> PendingRecovery | None:
"""Return the current :class:`~app.models.config.PendingRecovery` record.
A non-null response means fail2ban crashed shortly after a jail activation
and the user should be offered a rollback option. Returns ``null`` (HTTP
200 with ``null`` body) when no recovery is pending.
Args:
request: FastAPI request object.
_auth: Validated session.
Returns:
:class:`~app.models.config.PendingRecovery` or ``None``.
"""
return pending_recovery
@router.post(
"/{name}/rollback",
response_model=RollbackResponse,

View File

@@ -101,7 +101,7 @@ class TestGetJailConfigs:
jails=[_make_jail_config("sshd")], total=1
)
with patch(
"app.routers.config.config_service.list_jail_configs",
"app.routers.jail_config.config_service.list_jail_configs",
AsyncMock(return_value=mock_response),
):
resp = await config_client.get("/api/config/jails")
@@ -124,7 +124,7 @@ class TestGetJailConfigs:
from app.utils.fail2ban_client import Fail2BanConnectionError
with patch(
"app.routers.config.config_service.list_jail_configs",
"app.routers.jail_config.config_service.list_jail_configs",
AsyncMock(side_effect=Fail2BanConnectionError("down", "/tmp/fake.sock")),
):
resp = await config_client.get("/api/config/jails")
@@ -144,7 +144,7 @@ class TestGetJailConfig:
"""GET /api/config/jails/sshd returns 200 with JailConfigResponse."""
mock_response = JailConfigResponse(jail=_make_jail_config("sshd"))
with patch(
"app.routers.config.config_service.get_jail_config",
"app.routers.jail_config.config_service.get_jail_config",
AsyncMock(return_value=mock_response),
):
resp = await config_client.get("/api/config/jails/sshd")
@@ -158,7 +158,7 @@ class TestGetJailConfig:
from app.services.config_service import JailNotFoundError
with patch(
"app.routers.config.config_service.get_jail_config",
"app.routers.jail_config.config_service.get_jail_config",
AsyncMock(side_effect=JailNotFoundError("missing")),
):
resp = await config_client.get("/api/config/jails/missing")
@@ -185,7 +185,7 @@ class TestUpdateJailConfig:
async def test_204_on_success(self, config_client: AsyncClient) -> None:
"""PUT /api/config/jails/sshd returns 204 on success."""
with patch(
"app.routers.config.config_service.update_jail_config",
"app.routers.jail_config.config_service.update_jail_config",
AsyncMock(return_value=None),
):
resp = await config_client.put(
@@ -200,7 +200,7 @@ class TestUpdateJailConfig:
from app.services.config_service import JailNotFoundError
with patch(
"app.routers.config.config_service.update_jail_config",
"app.routers.jail_config.config_service.update_jail_config",
AsyncMock(side_effect=JailNotFoundError("missing")),
):
resp = await config_client.put(
@@ -215,7 +215,7 @@ class TestUpdateJailConfig:
from app.services.config_service import ConfigValidationError
with patch(
"app.routers.config.config_service.update_jail_config",
"app.routers.jail_config.config_service.update_jail_config",
AsyncMock(side_effect=ConfigValidationError("bad regex")),
):
resp = await config_client.put(
@@ -230,7 +230,7 @@ class TestUpdateJailConfig:
from app.services.config_service import ConfigOperationError
with patch(
"app.routers.config.config_service.update_jail_config",
"app.routers.jail_config.config_service.update_jail_config",
AsyncMock(side_effect=ConfigOperationError("set failed")),
):
resp = await config_client.put(
@@ -243,7 +243,7 @@ class TestUpdateJailConfig:
async def test_204_with_dns_mode(self, config_client: AsyncClient) -> None:
"""PUT /api/config/jails/sshd accepts dns_mode field."""
with patch(
"app.routers.config.config_service.update_jail_config",
"app.routers.jail_config.config_service.update_jail_config",
AsyncMock(return_value=None),
):
resp = await config_client.put(
@@ -256,7 +256,7 @@ class TestUpdateJailConfig:
async def test_204_with_prefregex(self, config_client: AsyncClient) -> None:
"""PUT /api/config/jails/sshd accepts prefregex field."""
with patch(
"app.routers.config.config_service.update_jail_config",
"app.routers.jail_config.config_service.update_jail_config",
AsyncMock(return_value=None),
):
resp = await config_client.put(
@@ -269,7 +269,7 @@ class TestUpdateJailConfig:
async def test_204_with_date_pattern(self, config_client: AsyncClient) -> None:
"""PUT /api/config/jails/sshd accepts date_pattern field."""
with patch(
"app.routers.config.config_service.update_jail_config",
"app.routers.jail_config.config_service.update_jail_config",
AsyncMock(return_value=None),
):
resp = await config_client.put(
@@ -297,7 +297,7 @@ class TestGetGlobalConfig:
db_max_matches=10,
)
with patch(
"app.routers.config.config_service.get_global_config",
"app.routers.config_misc.config_service.get_global_config",
AsyncMock(return_value=mock_response),
):
resp = await config_client.get("/api/config/global")
@@ -327,7 +327,7 @@ class TestUpdateGlobalConfig:
async def test_204_on_success(self, config_client: AsyncClient) -> None:
"""PUT /api/config/global returns 204 on success."""
with patch(
"app.routers.config.config_service.update_global_config",
"app.routers.config_misc.config_service.update_global_config",
AsyncMock(return_value=None),
):
resp = await config_client.put(
@@ -342,7 +342,7 @@ class TestUpdateGlobalConfig:
from app.services.config_service import ConfigOperationError
with patch(
"app.routers.config.config_service.update_global_config",
"app.routers.config_misc.config_service.update_global_config",
AsyncMock(side_effect=ConfigOperationError("set failed")),
):
resp = await config_client.put(
@@ -364,7 +364,7 @@ class TestReloadFail2ban:
async def test_204_on_success(self, config_client: AsyncClient) -> None:
"""POST /api/config/reload returns 204 on success."""
with patch(
"app.routers.config.jail_service.reload_all",
"app.routers.config_misc.jail_service.reload_all",
AsyncMock(return_value=None),
):
resp = await config_client.post("/api/config/reload")
@@ -376,7 +376,7 @@ class TestReloadFail2ban:
from app.utils.fail2ban_client import Fail2BanConnectionError
with patch(
"app.routers.config.jail_service.reload_all",
"app.routers.config_misc.jail_service.reload_all",
AsyncMock(side_effect=Fail2BanConnectionError("no socket", "/fake.sock")),
):
resp = await config_client.post("/api/config/reload")
@@ -388,7 +388,7 @@ class TestReloadFail2ban:
from app.services.jail_service import JailOperationError
with patch(
"app.routers.config.jail_service.reload_all",
"app.routers.config_misc.jail_service.reload_all",
AsyncMock(side_effect=JailOperationError("reload rejected")),
):
resp = await config_client.post("/api/config/reload")
@@ -408,15 +408,15 @@ class TestRestartFail2ban:
"""POST /api/config/restart returns 204 when fail2ban restarts cleanly."""
with (
patch(
"app.routers.config.jail_service.restart",
"app.routers.config_misc.jail_service.restart",
AsyncMock(return_value=None),
),
patch(
"app.routers.config.config_file_service.start_daemon",
"app.routers.config_misc.config_file_service.start_daemon",
AsyncMock(return_value=True),
),
patch(
"app.routers.config.config_file_service.wait_for_fail2ban",
"app.routers.config_misc.config_file_service.wait_for_fail2ban",
AsyncMock(return_value=True),
),
):
@@ -428,15 +428,15 @@ class TestRestartFail2ban:
"""POST /api/config/restart returns 503 when fail2ban does not come back online."""
with (
patch(
"app.routers.config.jail_service.restart",
"app.routers.config_misc.jail_service.restart",
AsyncMock(return_value=None),
),
patch(
"app.routers.config.config_file_service.start_daemon",
"app.routers.config_misc.config_file_service.start_daemon",
AsyncMock(return_value=True),
),
patch(
"app.routers.config.config_file_service.wait_for_fail2ban",
"app.routers.config_misc.config_file_service.wait_for_fail2ban",
AsyncMock(return_value=False),
),
):
@@ -449,7 +449,7 @@ class TestRestartFail2ban:
from app.services.jail_service import JailOperationError
with patch(
"app.routers.config.jail_service.restart",
"app.routers.config_misc.jail_service.restart",
AsyncMock(side_effect=JailOperationError("stop failed")),
):
resp = await config_client.post("/api/config/restart")
@@ -461,7 +461,7 @@ class TestRestartFail2ban:
from app.utils.fail2ban_client import Fail2BanConnectionError
with patch(
"app.routers.config.jail_service.restart",
"app.routers.config_misc.jail_service.restart",
AsyncMock(side_effect=Fail2BanConnectionError("no socket", "/fake.sock")),
):
resp = await config_client.post("/api/config/restart")
@@ -473,15 +473,15 @@ class TestRestartFail2ban:
mock_start = AsyncMock(return_value=True)
with (
patch(
"app.routers.config.jail_service.restart",
"app.routers.config_misc.jail_service.restart",
AsyncMock(return_value=None),
),
patch(
"app.routers.config.config_file_service.start_daemon",
"app.routers.config_misc.config_file_service.start_daemon",
mock_start,
),
patch(
"app.routers.config.config_file_service.wait_for_fail2ban",
"app.routers.config_misc.config_file_service.wait_for_fail2ban",
AsyncMock(return_value=True),
),
):
@@ -502,7 +502,7 @@ class TestRegexTest:
"""POST /api/config/regex-test returns matched=true for a valid match."""
mock_response = RegexTestResponse(matched=True, groups=["1.2.3.4"], error=None)
with patch(
"app.routers.config.log_service.test_regex",
"app.routers.config_misc.log_service.test_regex",
return_value=mock_response,
):
resp = await config_client.post(
@@ -520,7 +520,7 @@ class TestRegexTest:
"""POST /api/config/regex-test returns matched=false for no match."""
mock_response = RegexTestResponse(matched=False, groups=[], error=None)
with patch(
"app.routers.config.log_service.test_regex",
"app.routers.config_misc.log_service.test_regex",
return_value=mock_response,
):
resp = await config_client.post(
@@ -554,7 +554,7 @@ class TestAddLogPath:
async def test_204_on_success(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/sshd/logpath returns 204 on success."""
with patch(
"app.routers.config.config_service.add_log_path",
"app.routers.jail_config.config_service.add_log_path",
AsyncMock(return_value=None),
):
resp = await config_client.post(
@@ -569,7 +569,7 @@ class TestAddLogPath:
from app.services.config_service import JailNotFoundError
with patch(
"app.routers.config.config_service.add_log_path",
"app.routers.jail_config.config_service.add_log_path",
AsyncMock(side_effect=JailNotFoundError("missing")),
):
resp = await config_client.post(
@@ -598,7 +598,7 @@ class TestPreviewLog:
matched_count=1,
)
with patch(
"app.routers.config.log_service.preview_log",
"app.routers.config_misc.log_service.preview_log",
AsyncMock(return_value=mock_response),
):
resp = await config_client.post(
@@ -726,7 +726,7 @@ class TestGetInactiveJails:
mock_response = InactiveJailListResponse(jails=[mock_jail], total=1)
with patch(
"app.routers.config.jail_config_service.list_inactive_jails",
"app.routers.jail_config.jail_config_service.list_inactive_jails",
AsyncMock(return_value=mock_response),
):
resp = await config_client.get("/api/config/jails/inactive")
@@ -741,7 +741,7 @@ class TestGetInactiveJails:
from app.models.config import InactiveJailListResponse
with patch(
"app.routers.config.jail_config_service.list_inactive_jails",
"app.routers.jail_config.jail_config_service.list_inactive_jails",
AsyncMock(return_value=InactiveJailListResponse(jails=[], total=0)),
):
resp = await config_client.get("/api/config/jails/inactive")
@@ -777,7 +777,7 @@ class TestActivateJail:
message="Jail 'apache-auth' activated successfully.",
)
with patch(
"app.routers.config.jail_config_service.activate_jail",
"app.routers.jail_config.jail_config_service.activate_jail",
AsyncMock(return_value=mock_response),
):
resp = await config_client.post(
@@ -797,7 +797,7 @@ class TestActivateJail:
name="apache-auth", active=True, message="Activated."
)
with patch(
"app.routers.config.jail_config_service.activate_jail",
"app.routers.jail_config.jail_config_service.activate_jail",
AsyncMock(return_value=mock_response),
) as mock_activate:
resp = await config_client.post(
@@ -816,7 +816,7 @@ class TestActivateJail:
from app.services.jail_config_service import JailNotFoundInConfigError
with patch(
"app.routers.config.jail_config_service.activate_jail",
"app.routers.jail_config.jail_config_service.activate_jail",
AsyncMock(side_effect=JailNotFoundInConfigError("missing")),
):
resp = await config_client.post(
@@ -830,7 +830,7 @@ class TestActivateJail:
from app.services.jail_config_service import JailAlreadyActiveError
with patch(
"app.routers.config.jail_config_service.activate_jail",
"app.routers.jail_config.jail_config_service.activate_jail",
AsyncMock(side_effect=JailAlreadyActiveError("sshd")),
):
resp = await config_client.post(
@@ -841,10 +841,10 @@ class TestActivateJail:
async def test_400_for_invalid_jail_name(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/ with bad name returns 400."""
from app.services.jail_config_service import JailNameError
from app.exceptions import JailNameError
with patch(
"app.routers.config.jail_config_service.activate_jail",
"app.routers.jail_config.jail_config_service.activate_jail",
AsyncMock(side_effect=JailNameError("bad name")),
):
resp = await config_client.post(
@@ -873,7 +873,7 @@ class TestActivateJail:
message="Jail 'airsonic-auth' cannot be activated: log file '/var/log/airsonic/airsonic.log' not found",
)
with patch(
"app.routers.config.jail_config_service.activate_jail",
"app.routers.jail_config.jail_config_service.activate_jail",
AsyncMock(return_value=blocked_response),
):
resp = await config_client.post(
@@ -906,7 +906,7 @@ class TestDeactivateJail:
message="Jail 'sshd' deactivated successfully.",
)
with patch(
"app.routers.config.jail_config_service.deactivate_jail",
"app.routers.jail_config.jail_config_service.deactivate_jail",
AsyncMock(return_value=mock_response),
):
resp = await config_client.post("/api/config/jails/sshd/deactivate")
@@ -921,7 +921,7 @@ class TestDeactivateJail:
from app.services.jail_config_service import JailNotFoundInConfigError
with patch(
"app.routers.config.jail_config_service.deactivate_jail",
"app.routers.jail_config.jail_config_service.deactivate_jail",
AsyncMock(side_effect=JailNotFoundInConfigError("missing")),
):
resp = await config_client.post(
@@ -935,7 +935,7 @@ class TestDeactivateJail:
from app.services.jail_config_service import JailAlreadyInactiveError
with patch(
"app.routers.config.jail_config_service.deactivate_jail",
"app.routers.jail_config.jail_config_service.deactivate_jail",
AsyncMock(side_effect=JailAlreadyInactiveError("apache-auth")),
):
resp = await config_client.post(
@@ -946,10 +946,10 @@ class TestDeactivateJail:
async def test_400_for_invalid_jail_name(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/.../deactivate with bad name returns 400."""
from app.services.jail_config_service import JailNameError
from app.exceptions import JailNameError
with patch(
"app.routers.config.jail_config_service.deactivate_jail",
"app.routers.jail_config.jail_config_service.deactivate_jail",
AsyncMock(side_effect=JailNameError("bad")),
):
resp = await config_client.post(
@@ -977,7 +977,7 @@ class TestDeactivateJail:
)
with (
patch(
"app.routers.config.jail_config_service._deactivate_jail",
"app.routers.jail_config.jail_config_service._deactivate_jail",
AsyncMock(return_value=mock_response),
),
patch(
@@ -1028,7 +1028,7 @@ class TestListFilters:
total=1,
)
with patch(
"app.routers.config.filter_config_service.list_filters",
"app.routers.filter_config.filter_config_service.list_filters",
AsyncMock(return_value=mock_response),
):
resp = await config_client.get("/api/config/filters")
@@ -1044,7 +1044,7 @@ class TestListFilters:
from app.models.config import FilterListResponse
with patch(
"app.routers.config.filter_config_service.list_filters",
"app.routers.filter_config.filter_config_service.list_filters",
AsyncMock(return_value=FilterListResponse(filters=[], total=0)),
):
resp = await config_client.get("/api/config/filters")
@@ -1067,7 +1067,7 @@ class TestListFilters:
total=2,
)
with patch(
"app.routers.config.filter_config_service.list_filters",
"app.routers.filter_config.filter_config_service.list_filters",
AsyncMock(return_value=mock_response),
):
resp = await config_client.get("/api/config/filters")
@@ -1096,7 +1096,7 @@ class TestGetFilter:
async def test_200_returns_filter(self, config_client: AsyncClient) -> None:
"""GET /api/config/filters/sshd returns 200 with FilterConfig."""
with patch(
"app.routers.config.filter_config_service.get_filter",
"app.routers.filter_config.filter_config_service.get_filter",
AsyncMock(return_value=_make_filter_config("sshd")),
):
resp = await config_client.get("/api/config/filters/sshd")
@@ -1112,7 +1112,7 @@ class TestGetFilter:
from app.services.filter_config_service import FilterNotFoundError
with patch(
"app.routers.config.filter_config_service.get_filter",
"app.routers.filter_config.filter_config_service.get_filter",
AsyncMock(side_effect=FilterNotFoundError("missing")),
):
resp = await config_client.get("/api/config/filters/missing")
@@ -1139,7 +1139,7 @@ class TestUpdateFilter:
async def test_200_returns_updated_filter(self, config_client: AsyncClient) -> None:
"""PUT /api/config/filters/sshd returns 200 with updated FilterConfig."""
with patch(
"app.routers.config.filter_config_service.update_filter",
"app.routers.filter_config.filter_config_service.update_filter",
AsyncMock(return_value=_make_filter_config("sshd")),
):
resp = await config_client.put(
@@ -1155,7 +1155,7 @@ class TestUpdateFilter:
from app.services.filter_config_service import FilterNotFoundError
with patch(
"app.routers.config.filter_config_service.update_filter",
"app.routers.filter_config.filter_config_service.update_filter",
AsyncMock(side_effect=FilterNotFoundError("missing")),
):
resp = await config_client.put(
@@ -1170,7 +1170,7 @@ class TestUpdateFilter:
from app.services.filter_config_service import FilterInvalidRegexError
with patch(
"app.routers.config.filter_config_service.update_filter",
"app.routers.filter_config.filter_config_service.update_filter",
AsyncMock(side_effect=FilterInvalidRegexError("[bad", "unterminated")),
):
resp = await config_client.put(
@@ -1182,10 +1182,10 @@ class TestUpdateFilter:
async def test_400_for_invalid_name(self, config_client: AsyncClient) -> None:
"""PUT /api/config/filters/... with bad name returns 400."""
from app.services.filter_config_service import FilterNameError
from app.exceptions import FilterNameError
with patch(
"app.routers.config.filter_config_service.update_filter",
"app.routers.filter_config.filter_config_service.update_filter",
AsyncMock(side_effect=FilterNameError("bad")),
):
resp = await config_client.put(
@@ -1198,7 +1198,7 @@ class TestUpdateFilter:
async def test_reload_query_param_passed(self, config_client: AsyncClient) -> None:
"""PUT /api/config/filters/sshd?reload=true passes do_reload=True."""
with patch(
"app.routers.config.filter_config_service.update_filter",
"app.routers.filter_config.filter_config_service.update_filter",
AsyncMock(return_value=_make_filter_config("sshd")),
) as mock_update:
resp = await config_client.put(
@@ -1229,7 +1229,7 @@ class TestCreateFilter:
async def test_201_creates_filter(self, config_client: AsyncClient) -> None:
"""POST /api/config/filters returns 201 with FilterConfig."""
with patch(
"app.routers.config.filter_config_service.create_filter",
"app.routers.filter_config.filter_config_service.create_filter",
AsyncMock(return_value=_make_filter_config("my-custom")),
):
resp = await config_client.post(
@@ -1245,7 +1245,7 @@ class TestCreateFilter:
from app.services.filter_config_service import FilterAlreadyExistsError
with patch(
"app.routers.config.filter_config_service.create_filter",
"app.routers.filter_config.filter_config_service.create_filter",
AsyncMock(side_effect=FilterAlreadyExistsError("sshd")),
):
resp = await config_client.post(
@@ -1260,7 +1260,7 @@ class TestCreateFilter:
from app.services.filter_config_service import FilterInvalidRegexError
with patch(
"app.routers.config.filter_config_service.create_filter",
"app.routers.filter_config.filter_config_service.create_filter",
AsyncMock(side_effect=FilterInvalidRegexError("[bad", "unterminated")),
):
resp = await config_client.post(
@@ -1272,10 +1272,10 @@ class TestCreateFilter:
async def test_400_for_invalid_name(self, config_client: AsyncClient) -> None:
"""POST /api/config/filters returns 400 for invalid filter name."""
from app.services.filter_config_service import FilterNameError
from app.exceptions import FilterNameError
with patch(
"app.routers.config.filter_config_service.create_filter",
"app.routers.filter_config.filter_config_service.create_filter",
AsyncMock(side_effect=FilterNameError("bad")),
):
resp = await config_client.post(
@@ -1305,7 +1305,7 @@ class TestDeleteFilter:
async def test_204_deletes_filter(self, config_client: AsyncClient) -> None:
"""DELETE /api/config/filters/my-custom returns 204."""
with patch(
"app.routers.config.filter_config_service.delete_filter",
"app.routers.filter_config.filter_config_service.delete_filter",
AsyncMock(return_value=None),
):
resp = await config_client.delete("/api/config/filters/my-custom")
@@ -1317,7 +1317,7 @@ class TestDeleteFilter:
from app.services.filter_config_service import FilterNotFoundError
with patch(
"app.routers.config.filter_config_service.delete_filter",
"app.routers.filter_config.filter_config_service.delete_filter",
AsyncMock(side_effect=FilterNotFoundError("missing")),
):
resp = await config_client.delete("/api/config/filters/missing")
@@ -1329,7 +1329,7 @@ class TestDeleteFilter:
from app.services.filter_config_service import FilterReadonlyError
with patch(
"app.routers.config.filter_config_service.delete_filter",
"app.routers.filter_config.filter_config_service.delete_filter",
AsyncMock(side_effect=FilterReadonlyError("sshd")),
):
resp = await config_client.delete("/api/config/filters/sshd")
@@ -1338,10 +1338,10 @@ class TestDeleteFilter:
async def test_400_for_invalid_name(self, config_client: AsyncClient) -> None:
"""DELETE /api/config/filters/... with bad name returns 400."""
from app.services.filter_config_service import FilterNameError
from app.exceptions import FilterNameError
with patch(
"app.routers.config.filter_config_service.delete_filter",
"app.routers.filter_config.filter_config_service.delete_filter",
AsyncMock(side_effect=FilterNameError("bad")),
):
resp = await config_client.delete("/api/config/filters/bad")
@@ -1368,7 +1368,7 @@ class TestAssignFilterToJail:
async def test_204_assigns_filter(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/sshd/filter returns 204 on success."""
with patch(
"app.routers.config.filter_config_service.assign_filter_to_jail",
"app.routers.filter_config.filter_config_service.assign_filter_to_jail",
AsyncMock(return_value=None),
):
resp = await config_client.post(
@@ -1383,7 +1383,7 @@ class TestAssignFilterToJail:
from app.services.jail_config_service import JailNotFoundInConfigError
with patch(
"app.routers.config.filter_config_service.assign_filter_to_jail",
"app.routers.filter_config.filter_config_service.assign_filter_to_jail",
AsyncMock(side_effect=JailNotFoundInConfigError("missing")),
):
resp = await config_client.post(
@@ -1398,7 +1398,7 @@ class TestAssignFilterToJail:
from app.services.filter_config_service import FilterNotFoundError
with patch(
"app.routers.config.filter_config_service.assign_filter_to_jail",
"app.routers.filter_config.filter_config_service.assign_filter_to_jail",
AsyncMock(side_effect=FilterNotFoundError("missing-filter")),
):
resp = await config_client.post(
@@ -1410,10 +1410,10 @@ class TestAssignFilterToJail:
async def test_400_for_invalid_jail_name(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/.../filter with bad jail name returns 400."""
from app.services.jail_config_service import JailNameError
from app.exceptions import JailNameError
with patch(
"app.routers.config.filter_config_service.assign_filter_to_jail",
"app.routers.filter_config.filter_config_service.assign_filter_to_jail",
AsyncMock(side_effect=JailNameError("bad")),
):
resp = await config_client.post(
@@ -1425,10 +1425,10 @@ class TestAssignFilterToJail:
async def test_400_for_invalid_filter_name(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/sshd/filter with bad filter name returns 400."""
from app.services.filter_config_service import FilterNameError
from app.exceptions import FilterNameError
with patch(
"app.routers.config.filter_config_service.assign_filter_to_jail",
"app.routers.filter_config.filter_config_service.assign_filter_to_jail",
AsyncMock(side_effect=FilterNameError("bad")),
):
resp = await config_client.post(
@@ -1441,7 +1441,7 @@ class TestAssignFilterToJail:
async def test_reload_query_param_passed(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/sshd/filter?reload=true passes do_reload=True."""
with patch(
"app.routers.config.filter_config_service.assign_filter_to_jail",
"app.routers.filter_config.filter_config_service.assign_filter_to_jail",
AsyncMock(return_value=None),
) as mock_assign:
resp = await config_client.post(
@@ -1479,7 +1479,7 @@ class TestListActionsRouter:
mock_response = ActionListResponse(actions=[mock_action], total=1)
with patch(
"app.routers.config.action_config_service.list_actions",
"app.routers.action_config.action_config_service.list_actions",
AsyncMock(return_value=mock_response),
):
resp = await config_client.get("/api/config/actions")
@@ -1497,7 +1497,7 @@ class TestListActionsRouter:
mock_response = ActionListResponse(actions=[inactive, active], total=2)
with patch(
"app.routers.config.action_config_service.list_actions",
"app.routers.action_config.action_config_service.list_actions",
AsyncMock(return_value=mock_response),
):
resp = await config_client.get("/api/config/actions")
@@ -1525,7 +1525,7 @@ class TestGetActionRouter:
)
with patch(
"app.routers.config.action_config_service.get_action",
"app.routers.action_config.action_config_service.get_action",
AsyncMock(return_value=mock_action),
):
resp = await config_client.get("/api/config/actions/iptables")
@@ -1537,7 +1537,7 @@ class TestGetActionRouter:
from app.services.action_config_service import ActionNotFoundError
with patch(
"app.routers.config.action_config_service.get_action",
"app.routers.action_config.action_config_service.get_action",
AsyncMock(side_effect=ActionNotFoundError("missing")),
):
resp = await config_client.get("/api/config/actions/missing")
@@ -1564,7 +1564,7 @@ class TestUpdateActionRouter:
)
with patch(
"app.routers.config.action_config_service.update_action",
"app.routers.action_config.action_config_service.update_action",
AsyncMock(return_value=updated),
):
resp = await config_client.put(
@@ -1579,7 +1579,7 @@ class TestUpdateActionRouter:
from app.services.action_config_service import ActionNotFoundError
with patch(
"app.routers.config.action_config_service.update_action",
"app.routers.action_config.action_config_service.update_action",
AsyncMock(side_effect=ActionNotFoundError("missing")),
):
resp = await config_client.put(
@@ -1589,10 +1589,10 @@ class TestUpdateActionRouter:
assert resp.status_code == 404
async def test_400_for_bad_name(self, config_client: AsyncClient) -> None:
from app.services.action_config_service import ActionNameError
from app.exceptions import ActionNameError
with patch(
"app.routers.config.action_config_service.update_action",
"app.routers.action_config.action_config_service.update_action",
AsyncMock(side_effect=ActionNameError()),
):
resp = await config_client.put(
@@ -1621,7 +1621,7 @@ class TestCreateActionRouter:
)
with patch(
"app.routers.config.action_config_service.create_action",
"app.routers.action_config.action_config_service.create_action",
AsyncMock(return_value=created),
):
resp = await config_client.post(
@@ -1636,7 +1636,7 @@ class TestCreateActionRouter:
from app.services.action_config_service import ActionAlreadyExistsError
with patch(
"app.routers.config.action_config_service.create_action",
"app.routers.action_config.action_config_service.create_action",
AsyncMock(side_effect=ActionAlreadyExistsError("iptables")),
):
resp = await config_client.post(
@@ -1647,10 +1647,10 @@ class TestCreateActionRouter:
assert resp.status_code == 409
async def test_400_for_bad_name(self, config_client: AsyncClient) -> None:
from app.services.action_config_service import ActionNameError
from app.exceptions import ActionNameError
with patch(
"app.routers.config.action_config_service.create_action",
"app.routers.action_config.action_config_service.create_action",
AsyncMock(side_effect=ActionNameError()),
):
resp = await config_client.post(
@@ -1672,7 +1672,7 @@ class TestCreateActionRouter:
class TestDeleteActionRouter:
async def test_204_on_delete(self, config_client: AsyncClient) -> None:
with patch(
"app.routers.config.action_config_service.delete_action",
"app.routers.action_config.action_config_service.delete_action",
AsyncMock(return_value=None),
):
resp = await config_client.delete("/api/config/actions/custom")
@@ -1683,7 +1683,7 @@ class TestDeleteActionRouter:
from app.services.action_config_service import ActionNotFoundError
with patch(
"app.routers.config.action_config_service.delete_action",
"app.routers.action_config.action_config_service.delete_action",
AsyncMock(side_effect=ActionNotFoundError("missing")),
):
resp = await config_client.delete("/api/config/actions/missing")
@@ -1694,7 +1694,7 @@ class TestDeleteActionRouter:
from app.services.action_config_service import ActionReadonlyError
with patch(
"app.routers.config.action_config_service.delete_action",
"app.routers.action_config.action_config_service.delete_action",
AsyncMock(side_effect=ActionReadonlyError("iptables")),
):
resp = await config_client.delete("/api/config/actions/iptables")
@@ -1702,10 +1702,10 @@ class TestDeleteActionRouter:
assert resp.status_code == 409
async def test_400_for_bad_name(self, config_client: AsyncClient) -> None:
from app.services.action_config_service import ActionNameError
from app.exceptions import ActionNameError
with patch(
"app.routers.config.action_config_service.delete_action",
"app.routers.action_config.action_config_service.delete_action",
AsyncMock(side_effect=ActionNameError()),
):
resp = await config_client.delete("/api/config/actions/badname")
@@ -1724,7 +1724,7 @@ class TestDeleteActionRouter:
class TestAssignActionToJailRouter:
async def test_204_on_success(self, config_client: AsyncClient) -> None:
with patch(
"app.routers.config.action_config_service.assign_action_to_jail",
"app.routers.action_config.action_config_service.assign_action_to_jail",
AsyncMock(return_value=None),
):
resp = await config_client.post(
@@ -1738,7 +1738,7 @@ class TestAssignActionToJailRouter:
from app.services.jail_config_service import JailNotFoundInConfigError
with patch(
"app.routers.config.action_config_service.assign_action_to_jail",
"app.routers.action_config.action_config_service.assign_action_to_jail",
AsyncMock(side_effect=JailNotFoundInConfigError("missing")),
):
resp = await config_client.post(
@@ -1752,7 +1752,7 @@ class TestAssignActionToJailRouter:
from app.services.action_config_service import ActionNotFoundError
with patch(
"app.routers.config.action_config_service.assign_action_to_jail",
"app.routers.action_config.action_config_service.assign_action_to_jail",
AsyncMock(side_effect=ActionNotFoundError("missing")),
):
resp = await config_client.post(
@@ -1763,10 +1763,10 @@ class TestAssignActionToJailRouter:
assert resp.status_code == 404
async def test_400_for_bad_jail_name(self, config_client: AsyncClient) -> None:
from app.services.jail_config_service import JailNameError
from app.exceptions import JailNameError
with patch(
"app.routers.config.action_config_service.assign_action_to_jail",
"app.routers.action_config.action_config_service.assign_action_to_jail",
AsyncMock(side_effect=JailNameError()),
):
resp = await config_client.post(
@@ -1777,10 +1777,10 @@ class TestAssignActionToJailRouter:
assert resp.status_code == 400
async def test_400_for_bad_action_name(self, config_client: AsyncClient) -> None:
from app.services.action_config_service import ActionNameError
from app.exceptions import ActionNameError
with patch(
"app.routers.config.action_config_service.assign_action_to_jail",
"app.routers.action_config.action_config_service.assign_action_to_jail",
AsyncMock(side_effect=ActionNameError()),
):
resp = await config_client.post(
@@ -1792,7 +1792,7 @@ class TestAssignActionToJailRouter:
async def test_reload_param_passed(self, config_client: AsyncClient) -> None:
with patch(
"app.routers.config.action_config_service.assign_action_to_jail",
"app.routers.action_config.action_config_service.assign_action_to_jail",
AsyncMock(return_value=None),
) as mock_assign:
resp = await config_client.post(
@@ -1815,7 +1815,7 @@ class TestAssignActionToJailRouter:
class TestRemoveActionFromJailRouter:
async def test_204_on_success(self, config_client: AsyncClient) -> None:
with patch(
"app.routers.config.action_config_service.remove_action_from_jail",
"app.routers.action_config.action_config_service.remove_action_from_jail",
AsyncMock(return_value=None),
):
resp = await config_client.delete(
@@ -1828,7 +1828,7 @@ class TestRemoveActionFromJailRouter:
from app.services.jail_config_service import JailNotFoundInConfigError
with patch(
"app.routers.config.action_config_service.remove_action_from_jail",
"app.routers.action_config.action_config_service.remove_action_from_jail",
AsyncMock(side_effect=JailNotFoundInConfigError("missing")),
):
resp = await config_client.delete(
@@ -1838,10 +1838,10 @@ class TestRemoveActionFromJailRouter:
assert resp.status_code == 404
async def test_400_for_bad_jail_name(self, config_client: AsyncClient) -> None:
from app.services.jail_config_service import JailNameError
from app.exceptions import JailNameError
with patch(
"app.routers.config.action_config_service.remove_action_from_jail",
"app.routers.action_config.action_config_service.remove_action_from_jail",
AsyncMock(side_effect=JailNameError()),
):
resp = await config_client.delete(
@@ -1851,10 +1851,10 @@ class TestRemoveActionFromJailRouter:
assert resp.status_code == 400
async def test_400_for_bad_action_name(self, config_client: AsyncClient) -> None:
from app.services.action_config_service import ActionNameError
from app.exceptions import ActionNameError
with patch(
"app.routers.config.action_config_service.remove_action_from_jail",
"app.routers.action_config.action_config_service.remove_action_from_jail",
AsyncMock(side_effect=ActionNameError()),
):
resp = await config_client.delete(
@@ -1865,7 +1865,7 @@ class TestRemoveActionFromJailRouter:
async def test_reload_param_passed(self, config_client: AsyncClient) -> None:
with patch(
"app.routers.config.action_config_service.remove_action_from_jail",
"app.routers.action_config.action_config_service.remove_action_from_jail",
AsyncMock(return_value=None),
) as mock_rm:
resp = await config_client.delete(
@@ -1903,7 +1903,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.config_service.read_fail2ban_log",
"app.routers.config_misc.config_service.read_fail2ban_log",
AsyncMock(return_value=self._mock_log_response()),
):
resp = await config_client.get("/api/config/fail2ban-log")
@@ -1918,7 +1918,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.config_service.read_fail2ban_log",
"app.routers.config_misc.config_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 +1930,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.config_service.read_fail2ban_log",
"app.routers.config_misc.config_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 +1944,7 @@ class TestGetFail2BanLog:
from app.services.config_service import ConfigOperationError
with patch(
"app.routers.config.config_service.read_fail2ban_log",
"app.routers.config_misc.config_service.read_fail2ban_log",
AsyncMock(side_effect=ConfigOperationError("fail2ban is logging to 'STDOUT'")),
):
resp = await config_client.get("/api/config/fail2ban-log")
@@ -1956,7 +1956,7 @@ class TestGetFail2BanLog:
from app.services.config_service import ConfigOperationError
with patch(
"app.routers.config.config_service.read_fail2ban_log",
"app.routers.config_misc.config_service.read_fail2ban_log",
AsyncMock(side_effect=ConfigOperationError("outside the allowed directory")),
):
resp = await config_client.get("/api/config/fail2ban-log")
@@ -1968,7 +1968,7 @@ class TestGetFail2BanLog:
from app.utils.fail2ban_client import Fail2BanConnectionError
with patch(
"app.routers.config.config_service.read_fail2ban_log",
"app.routers.config_misc.config_service.read_fail2ban_log",
AsyncMock(side_effect=Fail2BanConnectionError("socket error", "/tmp/f.sock")),
):
resp = await config_client.get("/api/config/fail2ban-log")
@@ -2011,7 +2011,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.config_service.get_service_status",
"app.routers.config_misc.config_service.get_service_status",
AsyncMock(return_value=self._mock_status(online=True)),
):
resp = await config_client.get("/api/config/service-status")
@@ -2026,7 +2026,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.config_service.get_service_status",
"app.routers.config_misc.config_service.get_service_status",
AsyncMock(return_value=self._mock_status(online=False)),
):
resp = await config_client.get("/api/config/service-status")
@@ -2063,7 +2063,7 @@ class TestValidateJailEndpoint:
jail_name="sshd", valid=True, issues=[]
)
with patch(
"app.routers.config.jail_config_service.validate_jail_config",
"app.routers.jail_config.jail_config_service.validate_jail_config",
AsyncMock(return_value=mock_result),
):
resp = await config_client.post("/api/config/jails/sshd/validate")
@@ -2083,7 +2083,7 @@ class TestValidateJailEndpoint:
jail_name="sshd", valid=False, issues=[issue]
)
with patch(
"app.routers.config.jail_config_service.validate_jail_config",
"app.routers.jail_config.jail_config_service.validate_jail_config",
AsyncMock(return_value=mock_result),
):
resp = await config_client.post("/api/config/jails/sshd/validate")
@@ -2096,10 +2096,10 @@ class TestValidateJailEndpoint:
async def test_400_for_invalid_jail_name(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/bad-name/validate returns 400 on JailNameError."""
from app.services.jail_config_service import JailNameError
from app.exceptions import JailNameError
with patch(
"app.routers.config.jail_config_service.validate_jail_config",
"app.routers.jail_config.jail_config_service.validate_jail_config",
AsyncMock(side_effect=JailNameError("bad name")),
):
resp = await config_client.post("/api/config/jails/bad-name/validate")
@@ -2126,7 +2126,7 @@ class TestPendingRecovery:
app = config_client._transport.app # type: ignore[attr-defined]
app.state.pending_recovery = None
resp = await config_client.get("/api/config/pending-recovery")
resp = await config_client.get("/api/config/jails/pending-recovery")
assert resp.status_code == 200
assert resp.json() is None
@@ -2146,7 +2146,7 @@ class TestPendingRecovery:
app = config_client._transport.app # type: ignore[attr-defined]
app.state.pending_recovery = record
resp = await config_client.get("/api/config/pending-recovery")
resp = await config_client.get("/api/config/jails/pending-recovery")
assert resp.status_code == 200
data = resp.json()
@@ -2158,7 +2158,7 @@ class TestPendingRecovery:
resp = await AsyncClient(
transport=ASGITransport(app=config_client._transport.app), # type: ignore[attr-defined]
base_url="http://test",
).get("/api/config/pending-recovery")
).get("/api/config/jails/pending-recovery")
assert resp.status_code == 401
@@ -2191,7 +2191,7 @@ class TestRollbackEndpoint:
message="Jail 'sshd' disabled and fail2ban restarted.",
)
with patch(
"app.routers.config.jail_config_service._rollback_jail",
"app.routers.jail_config.jail_config_service._rollback_jail",
AsyncMock(return_value=mock_result),
):
resp = await config_client.post("/api/config/jails/sshd/rollback")
@@ -2228,7 +2228,7 @@ class TestRollbackEndpoint:
message="fail2ban did not come back online.",
)
with patch(
"app.routers.config.jail_config_service._rollback_jail",
"app.routers.jail_config.jail_config_service._rollback_jail",
AsyncMock(return_value=mock_result),
):
resp = await config_client.post("/api/config/jails/sshd/rollback")
@@ -2241,10 +2241,10 @@ class TestRollbackEndpoint:
async def test_400_for_invalid_jail_name(self, config_client: AsyncClient) -> None:
"""POST /api/config/jails/bad/rollback returns 400 on JailNameError."""
from app.services.jail_config_service import JailNameError
from app.exceptions import JailNameError
with patch(
"app.routers.config.jail_config_service.rollback_jail",
"app.routers.jail_config.jail_config_service.rollback_jail",
AsyncMock(side_effect=JailNameError("bad")),
):
resp = await config_client.post("/api/config/jails/bad/rollback")