fixed tests
This commit is contained in:
@@ -10,9 +10,13 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
|
||||
from app.exceptions import Fail2BanConnectionError
|
||||
from app.models.ban import ActiveBanListResponse, JailBannedIpsResponse
|
||||
from app.models.ban_domain import DomainActiveBanList
|
||||
from app.models.geo import GeoDetail, GeoInfo
|
||||
from app.models.jail import JailDetailResponse, JailListResponse
|
||||
from app.models.jail_domain import (
|
||||
DomainJailBannedIps,
|
||||
DomainJailDetail,
|
||||
DomainJailList,
|
||||
)
|
||||
from app.services import ban_service, jail_service
|
||||
from app.services.jail_service import JailNotFoundError, JailOperationError
|
||||
from app.utils import jail_socket
|
||||
@@ -109,9 +113,9 @@ class TestListJails:
|
||||
with _patch_client(responses):
|
||||
result = await jail_service.list_jails(_SOCKET, jail_service_state)
|
||||
|
||||
assert isinstance(result, JailListResponse)
|
||||
assert isinstance(result, DomainJailList)
|
||||
assert result.total == 1
|
||||
assert result.jails[0].name == "sshd"
|
||||
assert result.items[0].name == "sshd"
|
||||
|
||||
async def test_empty_jail_list(self, jail_service_state: JailServiceState) -> None:
|
||||
"""list_jails returns empty response when no jails are active."""
|
||||
@@ -120,7 +124,7 @@ class TestListJails:
|
||||
result = await jail_service.list_jails(_SOCKET, jail_service_state)
|
||||
|
||||
assert result.total == 0
|
||||
assert result.jails == []
|
||||
assert result.items == []
|
||||
|
||||
async def test_jail_status_populated(self, jail_service_state: JailServiceState) -> None:
|
||||
"""list_jails populates JailStatus with failed/banned counters."""
|
||||
@@ -136,7 +140,7 @@ class TestListJails:
|
||||
with _patch_client(responses):
|
||||
result = await jail_service.list_jails(_SOCKET, jail_service_state)
|
||||
|
||||
jail = result.jails[0]
|
||||
jail = result.items[0]
|
||||
assert jail.status is not None
|
||||
assert jail.status.currently_banned == 5
|
||||
assert jail.status.total_banned == 50
|
||||
@@ -155,7 +159,7 @@ class TestListJails:
|
||||
with _patch_client(responses):
|
||||
result = await jail_service.list_jails(_SOCKET, jail_service_state)
|
||||
|
||||
jail = result.jails[0]
|
||||
jail = result.items[0]
|
||||
assert jail.ban_time == 3600
|
||||
assert jail.find_time == 300
|
||||
assert jail.max_retry == 3
|
||||
@@ -183,7 +187,7 @@ class TestListJails:
|
||||
result = await jail_service.list_jails(_SOCKET, jail_service_state)
|
||||
|
||||
assert result.total == 2
|
||||
names = {j.name for j in result.jails}
|
||||
names = {j.name for j in result.items}
|
||||
assert names == {"sshd", "nginx"}
|
||||
|
||||
async def test_connection_error_propagates(self, jail_service_state: JailServiceState) -> None:
|
||||
@@ -223,7 +227,7 @@ class TestListJails:
|
||||
result = await jail_service.list_jails(_SOCKET, jail_service_state)
|
||||
|
||||
# Verify the result uses the default values for backend and idle.
|
||||
jail = result.jails[0]
|
||||
jail = result.items[0]
|
||||
assert jail.backend == "polling" # default
|
||||
assert jail.idle is False # default
|
||||
# Capability should now be cached as False.
|
||||
@@ -249,7 +253,7 @@ class TestListJails:
|
||||
result = await jail_service.list_jails(_SOCKET, jail_service_state)
|
||||
|
||||
# Verify real values are returned.
|
||||
jail = result.jails[0]
|
||||
jail = result.items[0]
|
||||
assert jail.backend == "systemd" # real value
|
||||
assert jail.idle is True # real value
|
||||
# Capability should now be cached as True.
|
||||
@@ -280,7 +284,7 @@ class TestListJails:
|
||||
result = await jail_service.list_jails(_SOCKET, jail_service_state)
|
||||
|
||||
# Both jails should return default values (cached result is False).
|
||||
for jail in result.jails:
|
||||
for jail in result.items:
|
||||
assert jail.backend == "polling"
|
||||
assert jail.idle is False
|
||||
|
||||
@@ -329,11 +333,11 @@ class TestGetJail:
|
||||
}
|
||||
|
||||
async def test_returns_jail_detail_response(self, jail_service_state: JailServiceState) -> None:
|
||||
"""get_jail returns a JailDetailResponse."""
|
||||
"""get_jail returns a DomainJailDetail."""
|
||||
with _patch_client(self._full_responses()):
|
||||
result = await jail_service.get_jail(_SOCKET, "sshd")
|
||||
|
||||
assert isinstance(result, JailDetailResponse)
|
||||
assert isinstance(result, DomainJailDetail)
|
||||
assert result.jail.name == "sshd"
|
||||
|
||||
async def test_log_paths_parsed(self, jail_service_state: JailServiceState) -> None:
|
||||
@@ -453,9 +457,7 @@ class TestJailControls:
|
||||
"reload|--all|[]|[['start', 'new'], ['start', 'nginx']]": (0, "OK"),
|
||||
}
|
||||
):
|
||||
await jail_service.reload_all(
|
||||
_SOCKET, include_jails=["new"], exclude_jails=["old"]
|
||||
)
|
||||
await jail_service.reload_all(_SOCKET, include_jails=["new"], exclude_jails=["old"])
|
||||
|
||||
async def test_reload_all_unknown_jail_raises_jail_not_found(self) -> None:
|
||||
"""reload_all detects UnknownJailException and raises JailNotFoundError.
|
||||
@@ -465,18 +467,19 @@ class TestJailControls:
|
||||
test verifies that reload_all detects this and re-raises as
|
||||
JailNotFoundError instead of the generic JailOperationError.
|
||||
"""
|
||||
with _patch_client(
|
||||
{
|
||||
"status": _make_global_status("sshd"),
|
||||
"reload|--all|[]|[['start', 'airsonic-auth'], ['start', 'sshd']]": (
|
||||
1,
|
||||
Exception("UnknownJailException('airsonic-auth')"),
|
||||
),
|
||||
}
|
||||
), pytest.raises(jail_service.JailNotFoundError) as exc_info:
|
||||
await jail_service.reload_all(
|
||||
_SOCKET, include_jails=["airsonic-auth"]
|
||||
)
|
||||
with (
|
||||
_patch_client(
|
||||
{
|
||||
"status": _make_global_status("sshd"),
|
||||
"reload|--all|[]|[['start', 'airsonic-auth'], ['start', 'sshd']]": (
|
||||
1,
|
||||
Exception("UnknownJailException('airsonic-auth')"),
|
||||
),
|
||||
}
|
||||
),
|
||||
pytest.raises(jail_service.JailNotFoundError) as exc_info,
|
||||
):
|
||||
await jail_service.reload_all(_SOCKET, include_jails=["airsonic-auth"])
|
||||
assert exc_info.value.name == "airsonic-auth"
|
||||
|
||||
async def test_restart_sends_stop_command(self) -> None:
|
||||
@@ -486,9 +489,7 @@ class TestJailControls:
|
||||
|
||||
async def test_restart_operation_error_raises(self) -> None:
|
||||
"""restart() raises JailOperationError when fail2ban rejects the stop."""
|
||||
with _patch_client({"stop": (1, Exception("cannot stop"))}), pytest.raises(
|
||||
JailOperationError
|
||||
):
|
||||
with _patch_client({"stop": (1, Exception("cannot stop"))}), pytest.raises(JailOperationError):
|
||||
await jail_service.restart(_SOCKET)
|
||||
|
||||
async def test_restart_connection_error_propagates(self) -> None:
|
||||
@@ -496,9 +497,7 @@ class TestJailControls:
|
||||
|
||||
class _FailClient:
|
||||
def __init__(self, **_kw: Any) -> None:
|
||||
self.send = AsyncMock(
|
||||
side_effect=Fail2BanConnectionError("no socket", _SOCKET)
|
||||
)
|
||||
self.send = AsyncMock(side_effect=Fail2BanConnectionError("no socket", _SOCKET))
|
||||
|
||||
with (
|
||||
patch("app.services.jail_service.Fail2BanClient", _FailClient),
|
||||
@@ -638,7 +637,7 @@ class TestGetActiveBans:
|
||||
with _patch_client(responses):
|
||||
result = await ban_service.get_active_bans(_SOCKET)
|
||||
|
||||
assert isinstance(result, ActiveBanListResponse)
|
||||
assert isinstance(result, DomainActiveBanList)
|
||||
assert result.total == 1
|
||||
assert result.bans[0].ip == "1.2.3.4"
|
||||
assert result.bans[0].jail == "sshd"
|
||||
@@ -724,17 +723,18 @@ class TestGetActiveBans:
|
||||
),
|
||||
}
|
||||
mock_geo = {"1.2.3.4": GeoInfo(country_code="DE", country_name="Germany", asn="AS1", org="ISP")}
|
||||
mock_batch = AsyncMock(return_value=mock_geo)
|
||||
mock_cache = AsyncMock()
|
||||
mock_cache.lookup_batch = AsyncMock(return_value=mock_geo)
|
||||
|
||||
with _patch_client(responses):
|
||||
mock_session = AsyncMock()
|
||||
result = await ban_service.get_active_bans(
|
||||
_SOCKET,
|
||||
http_session=mock_session,
|
||||
geo_batch_lookup=mock_batch,
|
||||
geo_cache=mock_cache,
|
||||
)
|
||||
|
||||
mock_batch.assert_awaited_once()
|
||||
mock_cache.lookup_batch.assert_awaited_once()
|
||||
assert result.total == 1
|
||||
assert result.bans[0].country == "DE"
|
||||
|
||||
@@ -748,14 +748,17 @@ class TestGetActiveBans:
|
||||
),
|
||||
}
|
||||
|
||||
failing_batch = AsyncMock(side_effect=RuntimeError("geo down"))
|
||||
import aiohttp
|
||||
|
||||
mock_cache = AsyncMock()
|
||||
mock_cache.lookup_batch = AsyncMock(side_effect=aiohttp.ClientError("geo down"))
|
||||
|
||||
with _patch_client(responses):
|
||||
mock_session = AsyncMock()
|
||||
result = await ban_service.get_active_bans(
|
||||
_SOCKET,
|
||||
http_session=mock_session,
|
||||
geo_batch_lookup=failing_batch,
|
||||
geo_cache=mock_cache,
|
||||
)
|
||||
|
||||
assert result.total == 1
|
||||
@@ -777,9 +780,7 @@ class TestGetActiveBans:
|
||||
return GeoInfo(country_code="JP", country_name="Japan", asn=None, org=None)
|
||||
|
||||
with _patch_client(responses):
|
||||
result = await ban_service.get_active_bans(
|
||||
_SOCKET, geo_enricher=_enricher
|
||||
)
|
||||
result = await ban_service.get_active_bans(_SOCKET, geo_enricher=_enricher)
|
||||
|
||||
assert result.total == 1
|
||||
assert result.bans[0].country == "JP"
|
||||
@@ -875,7 +876,7 @@ class TestLookupIp:
|
||||
assert result.geo.org == "Acme"
|
||||
|
||||
async def test_http_session_uses_geo_service_lookup(self) -> None:
|
||||
"""lookup_ip uses geo_service.lookup when http_session is provided."""
|
||||
"""lookup_ip uses geo_enricher when provided."""
|
||||
responses = {
|
||||
"get|--all|banned|1.2.3.4": (0, []),
|
||||
"status": _make_global_status("sshd"),
|
||||
@@ -883,19 +884,16 @@ class TestLookupIp:
|
||||
}
|
||||
|
||||
mock_geo = GeoInfo(country_code="JP", country_name="Japan", asn=None, org=None)
|
||||
mock_session = AsyncMock()
|
||||
mock_enricher = AsyncMock(return_value=mock_geo)
|
||||
|
||||
with _patch_client(responses), patch(
|
||||
"app.services.jail_service.geo_service.lookup",
|
||||
AsyncMock(return_value=mock_geo),
|
||||
) as mock_lookup:
|
||||
with _patch_client(responses):
|
||||
result = await jail_service.lookup_ip(
|
||||
_SOCKET,
|
||||
"1.2.3.4",
|
||||
http_session=mock_session,
|
||||
geo_enricher=mock_enricher,
|
||||
)
|
||||
|
||||
mock_lookup.assert_awaited_once_with("1.2.3.4", mock_session)
|
||||
mock_enricher.assert_awaited_once_with("1.2.3.4")
|
||||
assert isinstance(result.geo, GeoDetail)
|
||||
assert result.geo.country_code == "JP"
|
||||
assert result.geo.country_name == "Japan"
|
||||
@@ -985,7 +983,7 @@ class TestGetJailBannedIps:
|
||||
with _patch_client(_banned_ips_responses()):
|
||||
result = await jail_service.get_jail_banned_ips(_SOCKET, "sshd")
|
||||
|
||||
assert isinstance(result, JailBannedIpsResponse)
|
||||
assert isinstance(result, DomainJailBannedIps)
|
||||
|
||||
async def test_total_reflects_all_entries(self) -> None:
|
||||
"""total equals the number of parsed ban entries."""
|
||||
@@ -996,12 +994,8 @@ class TestGetJailBannedIps:
|
||||
|
||||
async def test_page_1_returns_first_n_items(self) -> None:
|
||||
"""page=1 with page_size=2 returns the first two entries."""
|
||||
with _patch_client(
|
||||
_banned_ips_responses(entries=[_BAN_ENTRY_1, _BAN_ENTRY_2, _BAN_ENTRY_3])
|
||||
):
|
||||
result = await jail_service.get_jail_banned_ips(
|
||||
_SOCKET, "sshd", page=1, page_size=2
|
||||
)
|
||||
with _patch_client(_banned_ips_responses(entries=[_BAN_ENTRY_1, _BAN_ENTRY_2, _BAN_ENTRY_3])):
|
||||
result = await jail_service.get_jail_banned_ips(_SOCKET, "sshd", page=1, page_size=2)
|
||||
|
||||
assert len(result.items) == 2
|
||||
assert result.items[0].ip == "1.2.3.4"
|
||||
@@ -1010,12 +1004,8 @@ class TestGetJailBannedIps:
|
||||
|
||||
async def test_page_2_returns_remaining_items(self) -> None:
|
||||
"""page=2 with page_size=2 returns the third entry."""
|
||||
with _patch_client(
|
||||
_banned_ips_responses(entries=[_BAN_ENTRY_1, _BAN_ENTRY_2, _BAN_ENTRY_3])
|
||||
):
|
||||
result = await jail_service.get_jail_banned_ips(
|
||||
_SOCKET, "sshd", page=2, page_size=2
|
||||
)
|
||||
with _patch_client(_banned_ips_responses(entries=[_BAN_ENTRY_1, _BAN_ENTRY_2, _BAN_ENTRY_3])):
|
||||
result = await jail_service.get_jail_banned_ips(_SOCKET, "sshd", page=2, page_size=2)
|
||||
|
||||
assert len(result.items) == 1
|
||||
assert result.items[0].ip == "9.10.11.12"
|
||||
@@ -1023,9 +1013,7 @@ class TestGetJailBannedIps:
|
||||
async def test_page_beyond_last_returns_empty_items(self) -> None:
|
||||
"""Requesting a page past the end returns an empty items list."""
|
||||
with _patch_client(_banned_ips_responses()):
|
||||
result = await jail_service.get_jail_banned_ips(
|
||||
_SOCKET, "sshd", page=99, page_size=25
|
||||
)
|
||||
result = await jail_service.get_jail_banned_ips(_SOCKET, "sshd", page=99, page_size=25)
|
||||
|
||||
assert result.items == []
|
||||
assert result.total == 2
|
||||
@@ -1033,9 +1021,7 @@ class TestGetJailBannedIps:
|
||||
async def test_search_filter_narrows_results(self) -> None:
|
||||
"""search parameter filters entries by IP substring."""
|
||||
with _patch_client(_banned_ips_responses()):
|
||||
result = await jail_service.get_jail_banned_ips(
|
||||
_SOCKET, "sshd", search="1.2.3"
|
||||
)
|
||||
result = await jail_service.get_jail_banned_ips(_SOCKET, "sshd", search="1.2.3")
|
||||
|
||||
assert result.total == 1
|
||||
assert result.items[0].ip == "1.2.3.4"
|
||||
@@ -1044,18 +1030,14 @@ class TestGetJailBannedIps:
|
||||
"""search filter is case-insensitive."""
|
||||
entries = ["192.168.0.1\t2025-01-01 10:00:00 + 600 = 2025-01-01 10:10:00"]
|
||||
with _patch_client(_banned_ips_responses(entries=entries)):
|
||||
result = await jail_service.get_jail_banned_ips(
|
||||
_SOCKET, "sshd", search="192.168"
|
||||
)
|
||||
result = await jail_service.get_jail_banned_ips(_SOCKET, "sshd", search="192.168")
|
||||
|
||||
assert result.total == 1
|
||||
|
||||
async def test_search_no_match_returns_empty(self) -> None:
|
||||
"""search that matches nothing returns empty items and total=0."""
|
||||
with _patch_client(_banned_ips_responses()):
|
||||
result = await jail_service.get_jail_banned_ips(
|
||||
_SOCKET, "sshd", search="999.999"
|
||||
)
|
||||
result = await jail_service.get_jail_banned_ips(_SOCKET, "sshd", search="999.999")
|
||||
|
||||
assert result.total == 0
|
||||
assert result.items == []
|
||||
@@ -1080,9 +1062,7 @@ class TestGetJailBannedIps:
|
||||
"get|sshd|banip|--with-time": (0, entries),
|
||||
}
|
||||
with _patch_client(responses):
|
||||
result = await jail_service.get_jail_banned_ips(
|
||||
_SOCKET, "sshd", page=1, page_size=200
|
||||
)
|
||||
result = await jail_service.get_jail_banned_ips(_SOCKET, "sshd", page=1, page_size=200)
|
||||
|
||||
assert len(result.items) <= 100
|
||||
|
||||
@@ -1090,30 +1070,22 @@ class TestGetJailBannedIps:
|
||||
"""Geo enrichment is requested only for IPs in the current page."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from app.services import geo_service
|
||||
|
||||
http_session = MagicMock()
|
||||
geo_enrichment_ips: list[list[str]] = []
|
||||
|
||||
async def _mock_lookup_batch(
|
||||
ips: list[str], _session: Any, **_kw: Any
|
||||
) -> dict[str, Any]:
|
||||
geo_enrichment_ips.append(list(ips))
|
||||
return {}
|
||||
mock_cache = MagicMock()
|
||||
mock_cache.lookup_batch = AsyncMock(
|
||||
side_effect=lambda ips, _session, **_kw: (geo_enrichment_ips.append(list(ips)), {})[-1]
|
||||
)
|
||||
|
||||
with (
|
||||
_patch_client(
|
||||
_banned_ips_responses(entries=[_BAN_ENTRY_1, _BAN_ENTRY_2, _BAN_ENTRY_3])
|
||||
),
|
||||
patch.object(geo_service, "lookup_batch", side_effect=_mock_lookup_batch),
|
||||
):
|
||||
with _patch_client(_banned_ips_responses(entries=[_BAN_ENTRY_1, _BAN_ENTRY_2, _BAN_ENTRY_3])):
|
||||
result = await jail_service.get_jail_banned_ips(
|
||||
_SOCKET,
|
||||
"sshd",
|
||||
page=1,
|
||||
page_size=2,
|
||||
http_session=http_session,
|
||||
geo_batch_lookup=geo_service.lookup_batch,
|
||||
geo_cache=mock_cache,
|
||||
)
|
||||
|
||||
# Only the 2-IP page slice should be passed to geo enrichment.
|
||||
@@ -1123,6 +1095,7 @@ class TestGetJailBannedIps:
|
||||
|
||||
async def test_unknown_jail_raises_jail_not_found_error(self) -> None:
|
||||
"""get_jail_banned_ips raises JailNotFoundError for unknown jail."""
|
||||
|
||||
# Simulate fail2ban returning an "unknown jail" error.
|
||||
class _FakeClient:
|
||||
def __init__(self, **_kw: Any) -> None:
|
||||
@@ -1142,9 +1115,7 @@ class TestGetJailBannedIps:
|
||||
|
||||
class _FailClient:
|
||||
def __init__(self, **_kw: Any) -> None:
|
||||
self.send = AsyncMock(
|
||||
side_effect=Fail2BanConnectionError("no socket", _SOCKET)
|
||||
)
|
||||
self.send = AsyncMock(side_effect=Fail2BanConnectionError("no socket", _SOCKET))
|
||||
|
||||
with (
|
||||
patch("app.services.jail_service.Fail2BanClient", _FailClient),
|
||||
|
||||
Reference in New Issue
Block a user