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

@@ -6,6 +6,7 @@ from pathlib import Path
import pytest
from app.models.config import ActionConfigUpdate, FilterConfigUpdate, JailFileConfigUpdate
from app.models.file_config import ConfFileCreateRequest, ConfFileUpdateRequest
from app.services.file_config_service import (
ConfigDirError,
@@ -18,13 +19,20 @@ from app.services.file_config_service import (
_validate_new_name,
create_action_file,
create_filter_file,
create_jail_config_file,
get_action_file,
get_filter_file,
get_jail_config_file,
get_parsed_action_file,
get_parsed_filter_file,
get_parsed_jail_file,
list_action_files,
list_filter_files,
list_jail_config_files,
set_jail_config_enabled,
update_parsed_action_file,
update_parsed_filter_file,
update_parsed_jail_file,
write_action_file,
write_filter_file,
)
@@ -399,3 +407,183 @@ async def test_create_action_file_creates_file(tmp_path: Path) -> None:
assert result == "my-action.conf"
assert (config_dir / "action.d" / "my-action.conf").is_file()
# ---------------------------------------------------------------------------
# create_jail_config_file
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_create_jail_config_file_creates_file(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
req = ConfFileCreateRequest(name="myjail", content="[myjail]\nenabled = true\n")
result = await create_jail_config_file(str(config_dir), req)
assert result == "myjail.conf"
assert (config_dir / "jail.d" / "myjail.conf").is_file()
@pytest.mark.asyncio
async def test_create_jail_config_file_conflict(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
(config_dir / "jail.d" / "sshd.conf").write_text("[sshd]\nenabled = true\n")
req = ConfFileCreateRequest(name="sshd", content="[sshd]\nenabled = true\n")
with pytest.raises(ConfigFileExistsError):
await create_jail_config_file(str(config_dir), req)
@pytest.mark.asyncio
async def test_create_jail_config_file_invalid_name(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
req = ConfFileCreateRequest(name="../escape", content="[x]\nenabled = true\n")
with pytest.raises(ConfigFileNameError):
await create_jail_config_file(str(config_dir), req)
# ---------------------------------------------------------------------------
# get_parsed_filter_file / update_parsed_filter_file
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_parsed_filter_file_returns_structured_model(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
content = "[Definition]\nfailregex = ^<HOST>\nignoreregex = ^.*ignore.*$\n"
(config_dir / "filter.d" / "nginx.conf").write_text(content)
result = await get_parsed_filter_file(str(config_dir), "nginx")
assert result.name == "nginx"
assert result.filename == "nginx.conf"
assert result.failregex == ["^<HOST>"]
assert result.ignoreregex == ["^.*ignore.*$"]
@pytest.mark.asyncio
async def test_get_parsed_filter_file_not_found(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
with pytest.raises(ConfigFileNotFoundError):
await get_parsed_filter_file(str(config_dir), "missing")
@pytest.mark.asyncio
async def test_update_parsed_filter_file_writes_changes(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
(config_dir / "filter.d" / "nginx.conf").write_text(
"[Definition]\nfailregex = ^<HOST> old\n"
)
update = FilterConfigUpdate(failregex=["^<HOST> new"])
await update_parsed_filter_file(str(config_dir), "nginx", update)
written = (config_dir / "filter.d" / "nginx.conf").read_text()
assert "new" in written
@pytest.mark.asyncio
async def test_update_parsed_filter_file_not_found(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
update = FilterConfigUpdate(failregex=["^<HOST>"])
with pytest.raises(ConfigFileNotFoundError):
await update_parsed_filter_file(str(config_dir), "missing", update)
# ---------------------------------------------------------------------------
# get_parsed_action_file / update_parsed_action_file
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_parsed_action_file_returns_structured_model(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
content = "[Definition]\nactionban = iptables -I INPUT -s <ip> -j DROP\n"
(config_dir / "action.d" / "iptables.conf").write_text(content)
result = await get_parsed_action_file(str(config_dir), "iptables")
assert result.name == "iptables"
assert result.filename == "iptables.conf"
assert result.actionban is not None
assert "<ip>" in result.actionban
@pytest.mark.asyncio
async def test_get_parsed_action_file_not_found(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
with pytest.raises(ConfigFileNotFoundError):
await get_parsed_action_file(str(config_dir), "missing")
@pytest.mark.asyncio
async def test_update_parsed_action_file_writes_changes(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
(config_dir / "action.d" / "iptables.conf").write_text(
"[Definition]\nactionban = iptables -I INPUT -s <ip> -j DROP\n"
)
update = ActionConfigUpdate(actionban="nft add element inet f2b-table <ip>")
await update_parsed_action_file(str(config_dir), "iptables", update)
written = (config_dir / "action.d" / "iptables.conf").read_text()
assert "nft" in written
@pytest.mark.asyncio
async def test_update_parsed_action_file_not_found(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
update = ActionConfigUpdate(actionban="iptables -I INPUT -s <ip> -j DROP")
with pytest.raises(ConfigFileNotFoundError):
await update_parsed_action_file(str(config_dir), "missing", update)
# ---------------------------------------------------------------------------
# get_parsed_jail_file / update_parsed_jail_file
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_parsed_jail_file_returns_structured_model(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
content = "[sshd]\nenabled = true\nport = ssh\nmaxretry = 5\n"
(config_dir / "jail.d" / "sshd.conf").write_text(content)
result = await get_parsed_jail_file(str(config_dir), "sshd.conf")
assert result.filename == "sshd.conf"
assert "sshd" in result.jails
assert result.jails["sshd"].enabled is True
assert result.jails["sshd"].maxretry == 5
@pytest.mark.asyncio
async def test_get_parsed_jail_file_not_found(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
with pytest.raises(ConfigFileNotFoundError):
await get_parsed_jail_file(str(config_dir), "missing.conf")
@pytest.mark.asyncio
async def test_update_parsed_jail_file_writes_changes(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
(config_dir / "jail.d" / "sshd.conf").write_text(
"[sshd]\nenabled = true\nport = ssh\n"
)
from app.models.config import JailSectionConfig
update = JailFileConfigUpdate(jails={"sshd": JailSectionConfig(enabled=False)})
await update_parsed_jail_file(str(config_dir), "sshd.conf", update)
written = (config_dir / "jail.d" / "sshd.conf").read_text()
assert "false" in written.lower()
@pytest.mark.asyncio
async def test_update_parsed_jail_file_not_found(tmp_path: Path) -> None:
config_dir = _make_config_dir(tmp_path)
update = JailFileConfigUpdate(jails={})
with pytest.raises(ConfigFileNotFoundError):
await update_parsed_jail_file(str(config_dir), "missing.conf", update)