On startup BanGUI now verifies that the four fail2ban jail config files required by its two custom jails (manual-Jail and blocklist-import) are present in `$fail2ban_config_dir/jail.d`. Any missing file is created with the correct default content; existing files are never overwritten. Files managed: - manual-Jail.conf (enabled=false template) - manual-Jail.local (enabled=true override) - blocklist-import.conf (enabled=false template) - blocklist-import.local (enabled=true override) The check runs in the lifespan hook immediately after logging is configured, before the database is opened.
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
"""Tests for app.utils.jail_config.ensure_jail_configs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from app.utils.jail_config import (
|
|
_BLOCKLIST_IMPORT_CONF,
|
|
_BLOCKLIST_IMPORT_LOCAL,
|
|
_MANUAL_JAIL_CONF,
|
|
_MANUAL_JAIL_LOCAL,
|
|
ensure_jail_configs,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Expected filenames
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_MANUAL_CONF = "manual-Jail.conf"
|
|
_MANUAL_LOCAL = "manual-Jail.local"
|
|
_BLOCKLIST_CONF = "blocklist-import.conf"
|
|
_BLOCKLIST_LOCAL = "blocklist-import.local"
|
|
|
|
_ALL_FILES = [_MANUAL_CONF, _MANUAL_LOCAL, _BLOCKLIST_CONF, _BLOCKLIST_LOCAL]
|
|
|
|
_CONTENT_MAP: dict[str, str] = {
|
|
_MANUAL_CONF: _MANUAL_JAIL_CONF,
|
|
_MANUAL_LOCAL: _MANUAL_JAIL_LOCAL,
|
|
_BLOCKLIST_CONF: _BLOCKLIST_IMPORT_CONF,
|
|
_BLOCKLIST_LOCAL: _BLOCKLIST_IMPORT_LOCAL,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _read(jail_d: Path, filename: str) -> str:
|
|
return (jail_d / filename).read_text(encoding="utf-8")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: ensure_jail_configs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestEnsureJailConfigs:
|
|
def test_all_missing_creates_all_four(self, tmp_path: Path) -> None:
|
|
"""All four files are created when the directory is empty."""
|
|
jail_d = tmp_path / "jail.d"
|
|
ensure_jail_configs(jail_d)
|
|
|
|
for name in _ALL_FILES:
|
|
assert (jail_d / name).exists(), f"{name} should have been created"
|
|
assert _read(jail_d, name) == _CONTENT_MAP[name]
|
|
|
|
def test_all_missing_creates_correct_content(self, tmp_path: Path) -> None:
|
|
"""Each created file has exactly the expected default content."""
|
|
jail_d = tmp_path / "jail.d"
|
|
ensure_jail_configs(jail_d)
|
|
|
|
# .conf files must set enabled = false
|
|
for conf_file in (_MANUAL_CONF, _BLOCKLIST_CONF):
|
|
content = _read(jail_d, conf_file)
|
|
assert "enabled = false" in content
|
|
|
|
# .local files must set enabled = true and nothing else
|
|
for local_file in (_MANUAL_LOCAL, _BLOCKLIST_LOCAL):
|
|
content = _read(jail_d, local_file)
|
|
assert "enabled = true" in content
|
|
|
|
def test_all_present_overwrites_nothing(self, tmp_path: Path) -> None:
|
|
"""Existing files are never overwritten."""
|
|
jail_d = tmp_path / "jail.d"
|
|
jail_d.mkdir()
|
|
|
|
sentinel = "# EXISTING CONTENT — must not be replaced\n"
|
|
for name in _ALL_FILES:
|
|
(jail_d / name).write_text(sentinel, encoding="utf-8")
|
|
|
|
ensure_jail_configs(jail_d)
|
|
|
|
for name in _ALL_FILES:
|
|
assert _read(jail_d, name) == sentinel, (
|
|
f"{name} should not have been overwritten"
|
|
)
|
|
|
|
def test_only_local_files_missing_creates_only_locals(
|
|
self, tmp_path: Path
|
|
) -> None:
|
|
"""Only the .local files are created when the .conf files already exist."""
|
|
jail_d = tmp_path / "jail.d"
|
|
jail_d.mkdir()
|
|
|
|
sentinel = "# pre-existing conf\n"
|
|
for conf_file in (_MANUAL_CONF, _BLOCKLIST_CONF):
|
|
(jail_d / conf_file).write_text(sentinel, encoding="utf-8")
|
|
|
|
ensure_jail_configs(jail_d)
|
|
|
|
# .conf files must remain unchanged
|
|
for conf_file in (_MANUAL_CONF, _BLOCKLIST_CONF):
|
|
assert _read(jail_d, conf_file) == sentinel
|
|
|
|
# .local files must have been created with correct content
|
|
for local_file, expected in (
|
|
(_MANUAL_LOCAL, _MANUAL_JAIL_LOCAL),
|
|
(_BLOCKLIST_LOCAL, _BLOCKLIST_IMPORT_LOCAL),
|
|
):
|
|
assert (jail_d / local_file).exists(), f"{local_file} should have been created"
|
|
assert _read(jail_d, local_file) == expected
|
|
|
|
def test_creates_jail_d_directory_if_missing(self, tmp_path: Path) -> None:
|
|
"""The jail.d directory is created automatically when absent."""
|
|
jail_d = tmp_path / "nested" / "jail.d"
|
|
assert not jail_d.exists()
|
|
ensure_jail_configs(jail_d)
|
|
assert jail_d.is_dir()
|
|
|
|
def test_idempotent_on_repeated_calls(self, tmp_path: Path) -> None:
|
|
"""Calling ensure_jail_configs twice does not alter any file."""
|
|
jail_d = tmp_path / "jail.d"
|
|
ensure_jail_configs(jail_d)
|
|
|
|
# Record content after first call
|
|
first_pass = {name: _read(jail_d, name) for name in _ALL_FILES}
|
|
|
|
ensure_jail_configs(jail_d)
|
|
|
|
for name in _ALL_FILES:
|
|
assert _read(jail_d, name) == first_pass[name], (
|
|
f"{name} changed on second call"
|
|
)
|