test(backend): add tests for conf-file parser, file-config service and router

- test_conffile_parser.py: unit tests for section/key parsing, comment
  preservation, and round-trip write correctness
- test_file_config_service.py: service-level tests with mock filesystem
- test_file_config.py: router integration tests covering GET / PUT
  endpoints for jails, actions, and filters
This commit is contained in:
2026-03-13 13:47:35 +01:00
parent 673eb4c7c2
commit f5c3635258
3 changed files with 1146 additions and 0 deletions

View File

@@ -12,6 +12,12 @@ from httpx import ASGITransport, AsyncClient
from app.config import Settings
from app.db import init_db
from app.main import create_app
from app.models.config import (
ActionConfig,
FilterConfig,
JailFileConfig,
JailSectionConfig,
)
from app.models.file_config import (
ConfFileContent,
ConfFileEntry,
@@ -377,3 +383,331 @@ class TestCreateActionFile:
assert resp.status_code == 201
assert resp.json()["filename"] == "myaction.conf"
# ---------------------------------------------------------------------------
# POST /api/config/jail-files
# ---------------------------------------------------------------------------
class TestCreateJailConfigFile:
async def test_201_creates_file(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.create_jail_config_file",
AsyncMock(return_value="myjail.conf"),
):
resp = await file_config_client.post(
"/api/config/jail-files",
json={"name": "myjail", "content": "[myjail]\nenabled = true\n"},
)
assert resp.status_code == 201
assert resp.json()["filename"] == "myjail.conf"
async def test_409_conflict(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.create_jail_config_file",
AsyncMock(side_effect=ConfigFileExistsError("myjail.conf")),
):
resp = await file_config_client.post(
"/api/config/jail-files",
json={"name": "myjail", "content": "[myjail]\nenabled = true\n"},
)
assert resp.status_code == 409
async def test_400_invalid_name(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.create_jail_config_file",
AsyncMock(side_effect=ConfigFileNameError("bad/../name")),
):
resp = await file_config_client.post(
"/api/config/jail-files",
json={"name": "../escape", "content": "[Definition]\n"},
)
assert resp.status_code == 400
async def test_503_on_config_dir_error(
self, file_config_client: AsyncClient
) -> None:
with patch(
"app.routers.file_config.file_config_service.create_jail_config_file",
AsyncMock(side_effect=ConfigDirError("no dir")),
):
resp = await file_config_client.post(
"/api/config/jail-files",
json={"name": "anyjail", "content": "[anyjail]\nenabled = false\n"},
)
assert resp.status_code == 503
# ---------------------------------------------------------------------------
# GET /api/config/filters/{name}/parsed
# ---------------------------------------------------------------------------
class TestGetParsedFilter:
async def test_200_returns_parsed_config(
self, file_config_client: AsyncClient
) -> None:
cfg = FilterConfig(name="nginx", filename="nginx.conf")
with patch(
"app.routers.file_config.file_config_service.get_parsed_filter_file",
AsyncMock(return_value=cfg),
):
resp = await file_config_client.get("/api/config/filters/nginx/parsed")
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "nginx"
assert data["filename"] == "nginx.conf"
async def test_404_not_found(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.get_parsed_filter_file",
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
):
resp = await file_config_client.get(
"/api/config/filters/missing/parsed"
)
assert resp.status_code == 404
async def test_503_on_config_dir_error(
self, file_config_client: AsyncClient
) -> None:
with patch(
"app.routers.file_config.file_config_service.get_parsed_filter_file",
AsyncMock(side_effect=ConfigDirError("no dir")),
):
resp = await file_config_client.get("/api/config/filters/nginx/parsed")
assert resp.status_code == 503
# ---------------------------------------------------------------------------
# PUT /api/config/filters/{name}/parsed
# ---------------------------------------------------------------------------
class TestUpdateParsedFilter:
async def test_204_on_success(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_filter_file",
AsyncMock(return_value=None),
):
resp = await file_config_client.put(
"/api/config/filters/nginx/parsed",
json={"failregex": ["^<HOST> "]},
)
assert resp.status_code == 204
async def test_404_not_found(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_filter_file",
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
):
resp = await file_config_client.put(
"/api/config/filters/missing/parsed",
json={"failregex": []},
)
assert resp.status_code == 404
async def test_400_write_error(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_filter_file",
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
):
resp = await file_config_client.put(
"/api/config/filters/nginx/parsed",
json={"failregex": ["^<HOST> "]},
)
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# GET /api/config/actions/{name}/parsed
# ---------------------------------------------------------------------------
class TestGetParsedAction:
async def test_200_returns_parsed_config(
self, file_config_client: AsyncClient
) -> None:
cfg = ActionConfig(name="iptables", filename="iptables.conf")
with patch(
"app.routers.file_config.file_config_service.get_parsed_action_file",
AsyncMock(return_value=cfg),
):
resp = await file_config_client.get(
"/api/config/actions/iptables/parsed"
)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "iptables"
assert data["filename"] == "iptables.conf"
async def test_404_not_found(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.get_parsed_action_file",
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
):
resp = await file_config_client.get(
"/api/config/actions/missing/parsed"
)
assert resp.status_code == 404
async def test_503_on_config_dir_error(
self, file_config_client: AsyncClient
) -> None:
with patch(
"app.routers.file_config.file_config_service.get_parsed_action_file",
AsyncMock(side_effect=ConfigDirError("no dir")),
):
resp = await file_config_client.get(
"/api/config/actions/iptables/parsed"
)
assert resp.status_code == 503
# ---------------------------------------------------------------------------
# PUT /api/config/actions/{name}/parsed
# ---------------------------------------------------------------------------
class TestUpdateParsedAction:
async def test_204_on_success(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_action_file",
AsyncMock(return_value=None),
):
resp = await file_config_client.put(
"/api/config/actions/iptables/parsed",
json={"actionban": "iptables -I INPUT -s <ip> -j DROP"},
)
assert resp.status_code == 204
async def test_404_not_found(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_action_file",
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
):
resp = await file_config_client.put(
"/api/config/actions/missing/parsed",
json={"actionban": ""},
)
assert resp.status_code == 404
async def test_400_write_error(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_action_file",
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
):
resp = await file_config_client.put(
"/api/config/actions/iptables/parsed",
json={"actionban": "iptables -I INPUT -s <ip> -j DROP"},
)
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# GET /api/config/jail-files/{filename}/parsed
# ---------------------------------------------------------------------------
class TestGetParsedJailFile:
async def test_200_returns_parsed_config(
self, file_config_client: AsyncClient
) -> None:
section = JailSectionConfig(enabled=True, port="ssh")
cfg = JailFileConfig(filename="sshd.conf", jails={"sshd": section})
with patch(
"app.routers.file_config.file_config_service.get_parsed_jail_file",
AsyncMock(return_value=cfg),
):
resp = await file_config_client.get(
"/api/config/jail-files/sshd.conf/parsed"
)
assert resp.status_code == 200
data = resp.json()
assert data["filename"] == "sshd.conf"
assert "sshd" in data["jails"]
async def test_404_not_found(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.get_parsed_jail_file",
AsyncMock(side_effect=ConfigFileNotFoundError("missing.conf")),
):
resp = await file_config_client.get(
"/api/config/jail-files/missing.conf/parsed"
)
assert resp.status_code == 404
async def test_503_on_config_dir_error(
self, file_config_client: AsyncClient
) -> None:
with patch(
"app.routers.file_config.file_config_service.get_parsed_jail_file",
AsyncMock(side_effect=ConfigDirError("no dir")),
):
resp = await file_config_client.get(
"/api/config/jail-files/sshd.conf/parsed"
)
assert resp.status_code == 503
# ---------------------------------------------------------------------------
# PUT /api/config/jail-files/{filename}/parsed
# ---------------------------------------------------------------------------
class TestUpdateParsedJailFile:
async def test_204_on_success(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_jail_file",
AsyncMock(return_value=None),
):
resp = await file_config_client.put(
"/api/config/jail-files/sshd.conf/parsed",
json={"jails": {"sshd": {"enabled": False}}},
)
assert resp.status_code == 204
async def test_404_not_found(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_jail_file",
AsyncMock(side_effect=ConfigFileNotFoundError("missing.conf")),
):
resp = await file_config_client.put(
"/api/config/jail-files/missing.conf/parsed",
json={"jails": {}},
)
assert resp.status_code == 404
async def test_400_write_error(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.update_parsed_jail_file",
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
):
resp = await file_config_client.put(
"/api/config/jail-files/sshd.conf/parsed",
json={"jails": {"sshd": {"enabled": True}}},
)
assert resp.status_code == 400