Add filter discovery endpoints with active/inactive status (Task 2.1)
- Add list_filters() and get_filter() to config_file_service.py:
scans filter.d/, parses [Definition] + [Init] sections, merges .local
overrides, and cross-references running jails to set active/used_by_jails
- Add FilterConfig.active, used_by_jails, source_file, has_local_override
fields to the Pydantic model; add FilterListResponse and FilterNotFoundError
- Add GET /api/config/filters and GET /api/config/filters/{name} to config.py
- Remove the shadowed GET /api/config/filters list route from file_config.py;
rename GET /api/config/filters/{name} raw variant to /filters/{name}/raw
- Update frontend: fetchFilterFiles() adapts FilterListResponse -> ConfFilesResponse;
add fetchFilters() and fetchFilter() to api/config.ts; remove unused
fetchFilterFiles/fetchActionFiles calls from useConfigActiveStatus
- Fix ConfigPageLogPath test mock to include fetchInactiveJails and related
exports introduced by Stage 1
- Backend: 169 tests pass, mypy --strict clean, ruff clean
- Frontend: 63 tests pass, tsc --noEmit clean, eslint clean
This commit is contained in:
@@ -13,6 +13,7 @@ from app.config import Settings
|
||||
from app.db import init_db
|
||||
from app.main import create_app
|
||||
from app.models.config import (
|
||||
FilterConfig,
|
||||
GlobalConfigResponse,
|
||||
JailConfig,
|
||||
JailConfigListResponse,
|
||||
@@ -817,3 +818,140 @@ class TestDeactivateJail:
|
||||
base_url="http://test",
|
||||
).post("/api/config/jails/sshd/deactivate")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/config/filters
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_filter_config(name: str, active: bool = False) -> FilterConfig:
|
||||
return FilterConfig(
|
||||
name=name,
|
||||
filename=f"{name}.conf",
|
||||
before=None,
|
||||
after=None,
|
||||
variables={},
|
||||
prefregex=None,
|
||||
failregex=[],
|
||||
ignoreregex=[],
|
||||
maxlines=None,
|
||||
datepattern=None,
|
||||
journalmatch=None,
|
||||
active=active,
|
||||
used_by_jails=[name] if active else [],
|
||||
source_file=f"/etc/fail2ban/filter.d/{name}.conf",
|
||||
has_local_override=False,
|
||||
)
|
||||
|
||||
|
||||
class TestListFilters:
|
||||
"""Tests for ``GET /api/config/filters``."""
|
||||
|
||||
async def test_200_returns_filter_list(self, config_client: AsyncClient) -> None:
|
||||
"""GET /api/config/filters returns 200 with FilterListResponse."""
|
||||
from app.models.config import FilterListResponse
|
||||
|
||||
mock_response = FilterListResponse(
|
||||
filters=[_make_filter_config("sshd", active=True)],
|
||||
total=1,
|
||||
)
|
||||
with patch(
|
||||
"app.routers.config.config_file_service.list_filters",
|
||||
AsyncMock(return_value=mock_response),
|
||||
):
|
||||
resp = await config_client.get("/api/config/filters")
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
assert data["filters"][0]["name"] == "sshd"
|
||||
assert data["filters"][0]["active"] is True
|
||||
|
||||
async def test_200_empty_filter_list(self, config_client: AsyncClient) -> None:
|
||||
"""GET /api/config/filters returns 200 with empty list when no filters found."""
|
||||
from app.models.config import FilterListResponse
|
||||
|
||||
with patch(
|
||||
"app.routers.config.config_file_service.list_filters",
|
||||
AsyncMock(return_value=FilterListResponse(filters=[], total=0)),
|
||||
):
|
||||
resp = await config_client.get("/api/config/filters")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 0
|
||||
assert resp.json()["filters"] == []
|
||||
|
||||
async def test_active_filters_sorted_before_inactive(
|
||||
self, config_client: AsyncClient
|
||||
) -> None:
|
||||
"""GET /api/config/filters returns active filters before inactive ones."""
|
||||
from app.models.config import FilterListResponse
|
||||
|
||||
mock_response = FilterListResponse(
|
||||
filters=[
|
||||
_make_filter_config("nginx", active=False),
|
||||
_make_filter_config("sshd", active=True),
|
||||
],
|
||||
total=2,
|
||||
)
|
||||
with patch(
|
||||
"app.routers.config.config_file_service.list_filters",
|
||||
AsyncMock(return_value=mock_response),
|
||||
):
|
||||
resp = await config_client.get("/api/config/filters")
|
||||
|
||||
data = resp.json()
|
||||
assert data["filters"][0]["name"] == "sshd" # active first
|
||||
assert data["filters"][1]["name"] == "nginx" # inactive second
|
||||
|
||||
async def test_401_when_unauthenticated(self, config_client: AsyncClient) -> None:
|
||||
"""GET /api/config/filters returns 401 without a valid session."""
|
||||
resp = await AsyncClient(
|
||||
transport=ASGITransport(app=config_client._transport.app), # type: ignore[attr-defined]
|
||||
base_url="http://test",
|
||||
).get("/api/config/filters")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/config/filters/{name}
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetFilter:
|
||||
"""Tests for ``GET /api/config/filters/{name}``."""
|
||||
|
||||
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.config_file_service.get_filter",
|
||||
AsyncMock(return_value=_make_filter_config("sshd")),
|
||||
):
|
||||
resp = await config_client.get("/api/config/filters/sshd")
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["name"] == "sshd"
|
||||
assert "failregex" in data
|
||||
assert "active" in data
|
||||
|
||||
async def test_404_for_unknown_filter(self, config_client: AsyncClient) -> None:
|
||||
"""GET /api/config/filters/missing returns 404."""
|
||||
from app.services.config_file_service import FilterNotFoundError
|
||||
|
||||
with patch(
|
||||
"app.routers.config.config_file_service.get_filter",
|
||||
AsyncMock(side_effect=FilterNotFoundError("missing")),
|
||||
):
|
||||
resp = await config_client.get("/api/config/filters/missing")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_401_when_unauthenticated(self, config_client: AsyncClient) -> None:
|
||||
"""GET /api/config/filters/sshd returns 401 without session."""
|
||||
resp = await AsyncClient(
|
||||
transport=ASGITransport(app=config_client._transport.app), # type: ignore[attr-defined]
|
||||
base_url="http://test",
|
||||
).get("/api/config/filters/sshd")
|
||||
assert resp.status_code == 401
|
||||
|
||||
@@ -218,45 +218,24 @@ class TestSetJailConfigEnabled:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/config/filters
|
||||
# GET /api/config/filters/{name}/raw
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestListFilterFiles:
|
||||
async def test_200_returns_files(self, file_config_client: AsyncClient) -> None:
|
||||
with patch(
|
||||
"app.routers.file_config.file_config_service.list_filter_files",
|
||||
AsyncMock(return_value=_conf_files_resp()),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/filters")
|
||||
class TestGetFilterFileRaw:
|
||||
"""Tests for the renamed ``GET /api/config/filters/{name}/raw`` endpoint.
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 1
|
||||
The simple list (``GET /api/config/filters``) and the structured detail
|
||||
(``GET /api/config/filters/{name}``) are now served by the config router.
|
||||
This endpoint returns the raw file content only.
|
||||
"""
|
||||
|
||||
async def test_503_on_config_dir_error(
|
||||
self, file_config_client: AsyncClient
|
||||
) -> None:
|
||||
with patch(
|
||||
"app.routers.file_config.file_config_service.list_filter_files",
|
||||
AsyncMock(side_effect=ConfigDirError("x")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/filters")
|
||||
|
||||
assert resp.status_code == 503
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/config/filters/{name}
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetFilterFile:
|
||||
async def test_200_returns_content(self, file_config_client: AsyncClient) -> None:
|
||||
with patch(
|
||||
"app.routers.file_config.file_config_service.get_filter_file",
|
||||
AsyncMock(return_value=_conf_file_content("nginx")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/filters/nginx")
|
||||
resp = await file_config_client.get("/api/config/filters/nginx/raw")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["name"] == "nginx"
|
||||
@@ -266,7 +245,7 @@ class TestGetFilterFile:
|
||||
"app.routers.file_config.file_config_service.get_filter_file",
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/filters/missing")
|
||||
resp = await file_config_client.get("/api/config/filters/missing/raw")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@@ -462,10 +462,9 @@ class TestActivateJail:
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
),
|
||||
),pytest.raises(JailNotFoundInConfigError)
|
||||
):
|
||||
with pytest.raises(JailNotFoundInConfigError):
|
||||
await activate_jail(str(tmp_path), "/fake.sock", "nonexistent", req)
|
||||
await activate_jail(str(tmp_path), "/fake.sock", "nonexistent", req)
|
||||
|
||||
async def test_raises_already_active(self, tmp_path: Path) -> None:
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
@@ -476,10 +475,9 @@ class TestActivateJail:
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value={"sshd"}),
|
||||
),
|
||||
),pytest.raises(JailAlreadyActiveError)
|
||||
):
|
||||
with pytest.raises(JailAlreadyActiveError):
|
||||
await activate_jail(str(tmp_path), "/fake.sock", "sshd", req)
|
||||
await activate_jail(str(tmp_path), "/fake.sock", "sshd", req)
|
||||
|
||||
async def test_raises_name_error_for_bad_name(self, tmp_path: Path) -> None:
|
||||
from app.models.config import ActivateJailRequest
|
||||
@@ -538,10 +536,9 @@ class TestDeactivateJail:
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value={"sshd"}),
|
||||
),
|
||||
),pytest.raises(JailNotFoundInConfigError)
|
||||
):
|
||||
with pytest.raises(JailNotFoundInConfigError):
|
||||
await deactivate_jail(str(tmp_path), "/fake.sock", "nonexistent")
|
||||
await deactivate_jail(str(tmp_path), "/fake.sock", "nonexistent")
|
||||
|
||||
async def test_raises_already_inactive(self, tmp_path: Path) -> None:
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
@@ -549,11 +546,309 @@ class TestDeactivateJail:
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
),
|
||||
),pytest.raises(JailAlreadyInactiveError)
|
||||
):
|
||||
with pytest.raises(JailAlreadyInactiveError):
|
||||
await deactivate_jail(str(tmp_path), "/fake.sock", "apache-auth")
|
||||
await deactivate_jail(str(tmp_path), "/fake.sock", "apache-auth")
|
||||
|
||||
async def test_raises_name_error(self, tmp_path: Path) -> None:
|
||||
with pytest.raises(JailNameError):
|
||||
await deactivate_jail(str(tmp_path), "/fake.sock", "a/b")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _extract_filter_base_name
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestExtractFilterBaseName:
|
||||
def test_simple_name(self) -> None:
|
||||
from app.services.config_file_service import _extract_filter_base_name
|
||||
|
||||
assert _extract_filter_base_name("sshd") == "sshd"
|
||||
|
||||
def test_name_with_mode(self) -> None:
|
||||
from app.services.config_file_service import _extract_filter_base_name
|
||||
|
||||
assert _extract_filter_base_name("sshd[mode=aggressive]") == "sshd"
|
||||
|
||||
def test_name_with_variable_mode(self) -> None:
|
||||
from app.services.config_file_service import _extract_filter_base_name
|
||||
|
||||
assert _extract_filter_base_name("sshd[mode=%(mode)s]") == "sshd"
|
||||
|
||||
def test_whitespace_stripped(self) -> None:
|
||||
from app.services.config_file_service import _extract_filter_base_name
|
||||
|
||||
assert _extract_filter_base_name(" nginx ") == "nginx"
|
||||
|
||||
def test_empty_string(self) -> None:
|
||||
from app.services.config_file_service import _extract_filter_base_name
|
||||
|
||||
assert _extract_filter_base_name("") == ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _build_filter_to_jails_map
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBuildFilterToJailsMap:
|
||||
def test_active_jail_maps_to_filter(self) -> None:
|
||||
from app.services.config_file_service import _build_filter_to_jails_map
|
||||
|
||||
result = _build_filter_to_jails_map({"sshd": {"filter": "sshd"}}, {"sshd"})
|
||||
assert result == {"sshd": ["sshd"]}
|
||||
|
||||
def test_inactive_jail_not_included(self) -> None:
|
||||
from app.services.config_file_service import _build_filter_to_jails_map
|
||||
|
||||
result = _build_filter_to_jails_map(
|
||||
{"apache-auth": {"filter": "apache-auth"}}, set()
|
||||
)
|
||||
assert result == {}
|
||||
|
||||
def test_multiple_jails_sharing_filter(self) -> None:
|
||||
from app.services.config_file_service import _build_filter_to_jails_map
|
||||
|
||||
all_jails = {
|
||||
"sshd": {"filter": "sshd"},
|
||||
"sshd-ddos": {"filter": "sshd"},
|
||||
}
|
||||
result = _build_filter_to_jails_map(all_jails, {"sshd", "sshd-ddos"})
|
||||
assert sorted(result["sshd"]) == ["sshd", "sshd-ddos"]
|
||||
|
||||
def test_mode_suffix_stripped(self) -> None:
|
||||
from app.services.config_file_service import _build_filter_to_jails_map
|
||||
|
||||
result = _build_filter_to_jails_map(
|
||||
{"sshd": {"filter": "sshd[mode=aggressive]"}}, {"sshd"}
|
||||
)
|
||||
assert "sshd" in result
|
||||
|
||||
def test_missing_filter_key_falls_back_to_jail_name(self) -> None:
|
||||
from app.services.config_file_service import _build_filter_to_jails_map
|
||||
|
||||
# When jail has no "filter" key the code falls back to the jail name.
|
||||
result = _build_filter_to_jails_map({"sshd": {}}, {"sshd"})
|
||||
assert "sshd" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _parse_filters_sync
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_FILTER_CONF = """\
|
||||
[Definition]
|
||||
failregex = ^Host: <HOST>
|
||||
ignoreregex =
|
||||
"""
|
||||
|
||||
|
||||
class TestParseFiltersSync:
|
||||
def test_returns_empty_for_missing_dir(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
|
||||
result = _parse_filters_sync(tmp_path / "nonexistent")
|
||||
assert result == []
|
||||
|
||||
def test_single_filter_returned(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "nginx.conf", _FILTER_CONF)
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
assert len(result) == 1
|
||||
name, filename, content, has_local = result[0]
|
||||
assert name == "nginx"
|
||||
assert filename == "nginx.conf"
|
||||
assert "failregex" in content
|
||||
assert has_local is False
|
||||
|
||||
def test_local_override_detected(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "nginx.conf", _FILTER_CONF)
|
||||
_write(filter_d / "nginx.local", "[Definition]\nignoreregex = ^safe\n")
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
_, _, _, has_local = result[0]
|
||||
assert has_local is True
|
||||
|
||||
def test_local_content_appended_to_content(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "nginx.conf", _FILTER_CONF)
|
||||
_write(filter_d / "nginx.local", "[Definition]\n# local tweak\n")
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
_, _, content, _ = result[0]
|
||||
assert "local tweak" in content
|
||||
|
||||
def test_sorted_alphabetically(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
for name in ("zzz", "aaa", "mmm"):
|
||||
_write(filter_d / f"{name}.conf", _FILTER_CONF)
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
names = [r[0] for r in result]
|
||||
assert names == ["aaa", "mmm", "zzz"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# list_filters
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestListFilters:
|
||||
async def test_returns_all_filters(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import list_filters
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
_write(filter_d / "nginx.conf", _FILTER_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await list_filters(str(tmp_path), "/fake.sock")
|
||||
|
||||
assert result.total == 2
|
||||
names = {f.name for f in result.filters}
|
||||
assert "sshd" in names
|
||||
assert "nginx" in names
|
||||
|
||||
async def test_active_flag_set_for_used_filter(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import list_filters
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value={"sshd"}),
|
||||
):
|
||||
result = await list_filters(str(tmp_path), "/fake.sock")
|
||||
|
||||
sshd = next(f for f in result.filters if f.name == "sshd")
|
||||
assert sshd.active is True
|
||||
assert "sshd" in sshd.used_by_jails
|
||||
|
||||
async def test_inactive_filter_not_marked_active(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import list_filters
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "nginx.conf", _FILTER_CONF)
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value={"sshd"}),
|
||||
):
|
||||
result = await list_filters(str(tmp_path), "/fake.sock")
|
||||
|
||||
nginx = next(f for f in result.filters if f.name == "nginx")
|
||||
assert nginx.active is False
|
||||
assert nginx.used_by_jails == []
|
||||
|
||||
async def test_has_local_override_detected(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import list_filters
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
_write(filter_d / "sshd.local", "[Definition]\n")
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await list_filters(str(tmp_path), "/fake.sock")
|
||||
|
||||
sshd = next(f for f in result.filters if f.name == "sshd")
|
||||
assert sshd.has_local_override is True
|
||||
|
||||
async def test_empty_filter_d_returns_empty(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import list_filters
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await list_filters(str(tmp_path), "/fake.sock")
|
||||
|
||||
assert result.filters == []
|
||||
assert result.total == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_filter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestGetFilter:
|
||||
async def test_returns_filter_config(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import get_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value={"sshd"}),
|
||||
):
|
||||
result = await get_filter(str(tmp_path), "/fake.sock", "sshd")
|
||||
|
||||
assert result.name == "sshd"
|
||||
assert result.active is True
|
||||
assert "sshd" in result.used_by_jails
|
||||
|
||||
async def test_accepts_conf_extension(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import get_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await get_filter(str(tmp_path), "/fake.sock", "sshd.conf")
|
||||
|
||||
assert result.name == "sshd"
|
||||
|
||||
async def test_raises_filter_not_found(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import FilterNotFoundError, get_filter
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), pytest.raises(FilterNotFoundError):
|
||||
await get_filter(str(tmp_path), "/fake.sock", "nonexistent")
|
||||
|
||||
async def test_has_local_override_detected(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import get_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
_write(filter_d / "sshd.local", "[Definition]\n")
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await get_filter(str(tmp_path), "/fake.sock", "sshd")
|
||||
|
||||
assert result.has_local_override is True
|
||||
|
||||
Reference in New Issue
Block a user