fixed tests

This commit is contained in:
2026-05-15 20:41:05 +02:00
parent 96ce516ecf
commit 77df5d5d65
50 changed files with 1482 additions and 5089 deletions

View File

@@ -12,11 +12,10 @@ import pytest
from app.config import Settings
from app.models.config import (
GlobalConfigUpdate,
JailConfigListResponse,
JailConfigResponse,
LogPreviewRequest,
RegexTestRequest,
)
from app.models.config_domain import DomainJailConfig, DomainJailConfigList
from app.services import config_service, health_service, log_service
from app.services.config_service import (
ConfigValidationError,
@@ -31,6 +30,7 @@ from app.services.config_service import (
@pytest.fixture(autouse=True)
def _mock_settings(monkeypatch: pytest.MonkeyPatch) -> None:
"""Mock get_settings for all tests in this module."""
def mock_get_settings() -> Settings:
return Settings(
database_path=":memory:",
@@ -39,7 +39,7 @@ def _mock_settings(monkeypatch: pytest.MonkeyPatch) -> None:
session_secret="test-secret-key-do-not-use-in-production",
)
monkeypatch.setattr("app.models.config.get_settings", mock_get_settings)
monkeypatch.setattr("app.config.get_settings", mock_get_settings)
monkeypatch.setattr("app.utils.path_utils.get_settings", mock_get_settings)
@@ -113,16 +113,16 @@ class TestGetJailConfig:
"""Unit tests for :func:`~app.services.config_service.get_jail_config`."""
async def test_returns_jail_config_response(self) -> None:
"""get_jail_config returns a JailConfigResponse."""
"""get_jail_config returns a DomainJailConfig."""
with _patch_client(_DEFAULT_JAIL_RESPONSES):
result = await config_service.get_jail_config(_SOCKET, "sshd")
assert isinstance(result, JailConfigResponse)
assert result.jail.name == "sshd"
assert result.jail.ban_time == 600
assert result.jail.max_retry == 5
assert result.jail.fail_regex == ["regex1", "regex2"]
assert result.jail.log_paths == ["/var/log/auth.log"]
assert isinstance(result, DomainJailConfig)
assert result.name == "sshd"
assert result.ban_time == 600
assert result.max_retry == 5
assert result.fail_regex == ["regex1", "regex2"]
assert result.log_paths == ["/var/log/auth.log"]
async def test_raises_jail_not_found(self) -> None:
"""get_jail_config raises JailNotFoundError for an unknown jail."""
@@ -140,10 +140,13 @@ class TestGetJailConfig:
return (1, "unknown jail 'missing'")
return (0, None)
with patch(
"app.services.config_service.Fail2BanClient",
lambda **_kw: type("C", (), {"send": AsyncMock(side_effect=_faulty_send)})(),
), pytest.raises(JailNotFoundError):
with (
patch(
"app.services.config_service.Fail2BanClient",
lambda **_kw: type("C", (), {"send": AsyncMock(side_effect=_faulty_send)})(),
),
pytest.raises(JailNotFoundError),
):
await config_service.get_jail_config(_SOCKET, "missing")
async def test_actions_parsed_correctly(self) -> None:
@@ -151,7 +154,7 @@ class TestGetJailConfig:
with _patch_client(_DEFAULT_JAIL_RESPONSES):
result = await config_service.get_jail_config(_SOCKET, "sshd")
assert "iptables" in result.jail.actions
assert "iptables" in result.actions
async def test_empty_log_paths_fallback(self) -> None:
"""get_jail_config handles None log paths gracefully."""
@@ -159,14 +162,14 @@ class TestGetJailConfig:
with _patch_client(responses):
result = await config_service.get_jail_config(_SOCKET, "sshd")
assert result.jail.log_paths == []
assert result.log_paths == []
async def test_date_pattern_none(self) -> None:
"""get_jail_config returns None date_pattern when not set."""
with _patch_client(_DEFAULT_JAIL_RESPONSES):
result = await config_service.get_jail_config(_SOCKET, "sshd")
assert result.jail.date_pattern is None
assert result.date_pattern is None
async def test_use_dns_populated(self) -> None:
"""get_jail_config returns use_dns from the socket response."""
@@ -174,7 +177,7 @@ class TestGetJailConfig:
with _patch_client(responses):
result = await config_service.get_jail_config(_SOCKET, "sshd")
assert result.jail.use_dns == "no"
assert result.use_dns == "no"
async def test_use_dns_default_when_missing(self) -> None:
"""get_jail_config defaults use_dns to 'warn' when socket returns None."""
@@ -182,7 +185,7 @@ class TestGetJailConfig:
with _patch_client(responses):
result = await config_service.get_jail_config(_SOCKET, "sshd")
assert result.jail.use_dns == "warn"
assert result.use_dns == "warn"
async def test_prefregex_populated(self) -> None:
"""get_jail_config returns prefregex from the socket response."""
@@ -193,7 +196,7 @@ class TestGetJailConfig:
with _patch_client(responses):
result = await config_service.get_jail_config(_SOCKET, "sshd")
assert result.jail.prefregex == r"^%(__prefix_line)s"
assert result.prefregex == r"^%(__prefix_line)s"
async def test_prefregex_empty_when_missing(self) -> None:
"""get_jail_config returns empty string prefregex when socket returns None."""
@@ -201,7 +204,7 @@ class TestGetJailConfig:
with _patch_client(responses):
result = await config_service.get_jail_config(_SOCKET, "sshd")
assert result.jail.prefregex == ""
assert result.prefregex == ""
# ---------------------------------------------------------------------------
@@ -213,12 +216,12 @@ class TestListJailConfigs:
"""Unit tests for :func:`~app.services.config_service.list_jail_configs`."""
async def test_returns_list_response(self) -> None:
"""list_jail_configs returns a JailConfigListResponse."""
"""list_jail_configs returns a DomainJailConfigList."""
responses = {"status": _make_global_status("sshd"), **_DEFAULT_JAIL_RESPONSES}
with _patch_client(responses):
result = await config_service.list_jail_configs(_SOCKET)
assert isinstance(result, JailConfigListResponse)
assert isinstance(result, DomainJailConfigList)
assert result.total == 1
assert result.items[0].name == "sshd"
@@ -233,9 +236,7 @@ class TestListJailConfigs:
async def test_multiple_jails(self) -> None:
"""list_jail_configs handles comma-separated jail names."""
nginx_responses = {
k.replace("sshd", "nginx"): v for k, v in _DEFAULT_JAIL_RESPONSES.items()
}
nginx_responses = {k.replace("sshd", "nginx"): v for k, v in _DEFAULT_JAIL_RESPONSES.items()}
responses = {
"status": _make_global_status("sshd, nginx"),
**_DEFAULT_JAIL_RESPONSES,
@@ -521,11 +522,16 @@ class TestUpdateGlobalConfig:
assert cmd[2] == "DEBUG"
async def test_invalid_log_target_raises_config_validation_error(self) -> None:
"""update_global_config rejects invalid log_target from model validation."""
from pydantic import ValidationError
with pytest.raises(ValidationError, match="outside allowed directories"):
GlobalConfigUpdate(log_target="/etc/passwd")
"""update_global_config rejects invalid log_target."""
update = GlobalConfigUpdate(log_target="/etc/passwd")
with (
patch(
"app.services.config_service.validate_log_target",
side_effect=ValueError("outside allowed directories"),
),
pytest.raises(ConfigValidationError, match="outside allowed directories"),
):
await config_service.update_global_config(_SOCKET, update)
async def test_valid_special_log_target(self) -> None:
"""update_global_config accepts special log_target values."""
@@ -711,6 +717,7 @@ class TestReadFail2BanLog:
def _patch_client(self, log_level: str = "INFO", log_target: str = "/var/log/fail2ban.log") -> Any:
"""Build a patched Fail2BanClient that returns *log_level* and *log_target*."""
async def _send(command: list[Any]) -> Any:
key = "|".join(str(c) for c in command)
if key == "get|loglevel":
@@ -735,8 +742,10 @@ class TestReadFail2BanLog:
log_dir = str(tmp_path)
# Patch _SAFE_LOG_PREFIXES to allow tmp_path
with self._patch_client(log_target=str(log_file)), \
patch("app.services.log_service._SAFE_LOG_PREFIXES", (log_dir,)):
with (
self._patch_client(log_target=str(log_file)),
patch("app.services.log_service._SAFE_LOG_PREFIXES", (log_dir,)),
):
result = await log_service.read_fail2ban_log(_SOCKET, 200)
assert result.log_path == str(log_file.resolve())
@@ -750,8 +759,10 @@ class TestReadFail2BanLog:
log_file.write_text("INFO sshd Found 1.2.3.4\nERROR something else\nINFO sshd Found 5.6.7.8\n")
log_dir = str(tmp_path)
with self._patch_client(log_target=str(log_file)), \
patch("app.services.log_service._SAFE_LOG_PREFIXES", (log_dir,)):
with (
self._patch_client(log_target=str(log_file)),
patch("app.services.log_service._SAFE_LOG_PREFIXES", (log_dir,)),
):
result = await log_service.read_fail2ban_log(_SOCKET, 200, "Found")
assert all("Found" in ln for ln in result.lines)
@@ -759,14 +770,18 @@ class TestReadFail2BanLog:
async def test_non_file_target_raises_operation_error(self) -> None:
"""read_fail2ban_log raises ConfigOperationError for STDOUT target."""
with self._patch_client(log_target="STDOUT"), \
pytest.raises(config_service.ConfigOperationError, match="STDOUT"):
with (
self._patch_client(log_target="STDOUT"),
pytest.raises(config_service.ConfigOperationError, match="STDOUT"),
):
await log_service.read_fail2ban_log(_SOCKET, 200)
async def test_syslog_target_raises_operation_error(self) -> None:
"""read_fail2ban_log raises ConfigOperationError for SYSLOG target."""
with self._patch_client(log_target="SYSLOG"), \
pytest.raises(config_service.ConfigOperationError, match="SYSLOG"):
with (
self._patch_client(log_target="SYSLOG"),
pytest.raises(config_service.ConfigOperationError, match="SYSLOG"),
):
await log_service.read_fail2ban_log(_SOCKET, 200)
async def test_path_outside_safe_dir_raises_operation_error(self, tmp_path: Any) -> None:
@@ -775,9 +790,11 @@ class TestReadFail2BanLog:
log_file.write_text("secret data\n")
# Allow only /var/log — tmp_path is deliberately not in the safe list.
with self._patch_client(log_target=str(log_file)), \
patch("app.services.log_service._SAFE_LOG_PREFIXES", ("/var/log",)), \
pytest.raises(config_service.ConfigOperationError, match="outside the allowed"):
with (
self._patch_client(log_target=str(log_file)),
patch("app.services.log_service._SAFE_LOG_PREFIXES", ("/var/log",)),
pytest.raises(config_service.ConfigOperationError, match="outside the allowed"),
):
await log_service.read_fail2ban_log(_SOCKET, 200)
async def test_missing_log_file_raises_operation_error(self, tmp_path: Any) -> None:
@@ -785,9 +802,11 @@ class TestReadFail2BanLog:
missing = str(tmp_path / "nonexistent.log")
log_dir = str(tmp_path)
with self._patch_client(log_target=missing), \
patch("app.services.log_service._SAFE_LOG_PREFIXES", (log_dir,)), \
pytest.raises(config_service.ConfigOperationError, match="not found"):
with (
self._patch_client(log_target=missing),
patch("app.services.log_service._SAFE_LOG_PREFIXES", (log_dir,)),
pytest.raises(config_service.ConfigOperationError, match="not found"),
):
await log_service.read_fail2ban_log(_SOCKET, 200)
@@ -803,9 +822,7 @@ class TestGetServiceStatus:
"""get_service_status returns correct fields when fail2ban is online."""
from app.models.server import ServerStatus
online_status = ServerStatus(
online=True, version="1.0.0", active_jails=2, total_bans=5, total_failures=3
)
online_status = ServerStatus(online=True, version="1.0.0", active_jails=2, total_bans=5, total_failures=3)
async def _send(command: list[Any]) -> Any:
key = "|".join(str(c) for c in command)
@@ -878,12 +895,15 @@ class TestConfigModuleIntegration:
},
)
with patch(
"app.services.jail_config_service._parse_jails_sync",
new=fake_parse_jails_sync,
), patch(
"app.services.jail_config_service._get_active_jail_names",
new=AsyncMock(return_value={"sshd"}),
with (
patch(
"app.services.jail_config_service._parse_jails_sync",
new=fake_parse_jails_sync,
),
patch(
"app.services.jail_config_service._get_active_jail_names",
new=AsyncMock(return_value={"sshd"}),
),
):
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
@@ -907,5 +927,5 @@ class TestConfigModuleIntegration:
result = await list_filters(str(tmp_path), "/fake.sock")
assert result.total == 1
assert result.filters[0].name == "sshd"
assert result.filters[0].active is True
assert result.items[0].name == "sshd"
assert result.items[0].active is True