Refactor ban management with domain models and mappers
- Add ban domain model for core business logic separation - Implement mapper pattern for DTO/domain conversions - Update ban service with new domain-driven approach - Refactor router endpoints to use new architecture - Add comprehensive mapper tests - Update documentation with architecture changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1
backend/tests/test_mappers/__init__.py
Normal file
1
backend/tests/test_mappers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for response mappers."""
|
||||
220
backend/tests/test_mappers/test_ban_mappers.py
Normal file
220
backend/tests/test_mappers/test_ban_mappers.py
Normal file
@@ -0,0 +1,220 @@
|
||||
"""Tests for ban response mappers."""
|
||||
|
||||
from app.mappers import (
|
||||
map_domain_active_ban_list_to_response,
|
||||
map_domain_active_ban_to_response,
|
||||
map_domain_bans_by_country_to_response,
|
||||
map_domain_bans_by_jail_to_response,
|
||||
map_domain_ban_trend_to_response,
|
||||
map_domain_dashboard_ban_item_to_response,
|
||||
map_domain_dashboard_ban_list_to_response,
|
||||
)
|
||||
from app.models.ban_domain import (
|
||||
DomainActiveBan,
|
||||
DomainActiveBanList,
|
||||
DomainBansByCountry,
|
||||
DomainBansByJail,
|
||||
DomainBanTrend,
|
||||
DomainBanTrendBucket,
|
||||
DomainDashboardBanItem,
|
||||
DomainDashboardBanList,
|
||||
DomainJailBanCount,
|
||||
)
|
||||
|
||||
|
||||
class TestActiveBanMapper:
|
||||
"""Test mapping from DomainActiveBan to ActiveBan."""
|
||||
|
||||
def test_maps_all_fields(self) -> None:
|
||||
"""All fields are correctly mapped."""
|
||||
domain_ban = DomainActiveBan(
|
||||
ip="192.168.1.1",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
expires_at="2026-04-28T08:00:00+00:00",
|
||||
ban_count=3,
|
||||
country="DE",
|
||||
)
|
||||
|
||||
result = map_domain_active_ban_to_response(domain_ban)
|
||||
|
||||
assert result.ip == "192.168.1.1"
|
||||
assert result.jail == "sshd"
|
||||
assert result.banned_at == "2026-04-28T07:00:00+00:00"
|
||||
assert result.expires_at == "2026-04-28T08:00:00+00:00"
|
||||
assert result.ban_count == 3
|
||||
assert result.country == "DE"
|
||||
|
||||
def test_handles_null_timestamps(self) -> None:
|
||||
"""Null timestamps are preserved."""
|
||||
domain_ban = DomainActiveBan(
|
||||
ip="10.0.0.1",
|
||||
jail="test",
|
||||
banned_at=None,
|
||||
expires_at=None,
|
||||
)
|
||||
|
||||
result = map_domain_active_ban_to_response(domain_ban)
|
||||
|
||||
assert result.banned_at is None
|
||||
assert result.expires_at is None
|
||||
|
||||
|
||||
class TestActiveBanListMapper:
|
||||
"""Test mapping from DomainActiveBanList to ActiveBanListResponse."""
|
||||
|
||||
def test_maps_list_and_total(self) -> None:
|
||||
"""List and total are correctly mapped."""
|
||||
domain_list = DomainActiveBanList(
|
||||
bans=[
|
||||
DomainActiveBan(ip="1.1.1.1", jail="sshd", ban_count=1),
|
||||
DomainActiveBan(ip="2.2.2.2", jail="httpd", ban_count=2),
|
||||
],
|
||||
total=2,
|
||||
)
|
||||
|
||||
result = map_domain_active_ban_list_to_response(domain_list)
|
||||
|
||||
assert result.total == 2
|
||||
assert len(result.bans) == 2
|
||||
assert result.bans[0].ip == "1.1.1.1"
|
||||
assert result.bans[1].ip == "2.2.2.2"
|
||||
|
||||
def test_handles_empty_list(self) -> None:
|
||||
"""Empty list is handled correctly."""
|
||||
domain_list = DomainActiveBanList(bans=[], total=0)
|
||||
|
||||
result = map_domain_active_ban_list_to_response(domain_list)
|
||||
|
||||
assert result.total == 0
|
||||
assert len(result.bans) == 0
|
||||
|
||||
|
||||
class TestDashboardBanItemMapper:
|
||||
"""Test mapping from DomainDashboardBanItem to DashboardBanItem."""
|
||||
|
||||
def test_maps_all_fields(self) -> None:
|
||||
"""All fields are correctly mapped."""
|
||||
domain_item = DomainDashboardBanItem(
|
||||
ip="203.0.113.1",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
service="SSH login attempt",
|
||||
country_code="US",
|
||||
country_name="United States",
|
||||
asn="AS15169",
|
||||
org="Google LLC",
|
||||
ban_count=5,
|
||||
origin="selfblock",
|
||||
)
|
||||
|
||||
result = map_domain_dashboard_ban_item_to_response(domain_item)
|
||||
|
||||
assert result.ip == "203.0.113.1"
|
||||
assert result.jail == "sshd"
|
||||
assert result.banned_at == "2026-04-28T07:00:00+00:00"
|
||||
assert result.service == "SSH login attempt"
|
||||
assert result.country_code == "US"
|
||||
assert result.country_name == "United States"
|
||||
assert result.asn == "AS15169"
|
||||
assert result.org == "Google LLC"
|
||||
assert result.ban_count == 5
|
||||
assert result.origin == "selfblock"
|
||||
|
||||
|
||||
class TestDashboardBanListMapper:
|
||||
"""Test mapping from DomainDashboardBanList to DashboardBanListResponse."""
|
||||
|
||||
def test_maps_pagination_and_items(self) -> None:
|
||||
"""Pagination metadata and items are correctly mapped."""
|
||||
domain_list = DomainDashboardBanList(
|
||||
items=[
|
||||
DomainDashboardBanItem(
|
||||
ip="1.1.1.1",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
ban_count=1,
|
||||
),
|
||||
],
|
||||
total=100,
|
||||
page=2,
|
||||
page_size=50,
|
||||
)
|
||||
|
||||
result = map_domain_dashboard_ban_list_to_response(domain_list)
|
||||
|
||||
assert result.total == 100
|
||||
assert result.page == 2
|
||||
assert result.page_size == 50
|
||||
assert len(result.items) == 1
|
||||
assert result.items[0].ip == "1.1.1.1"
|
||||
|
||||
|
||||
class TestBansByCountryMapper:
|
||||
"""Test mapping from DomainBansByCountry to BansByCountryResponse."""
|
||||
|
||||
def test_maps_aggregation_and_items(self) -> None:
|
||||
"""Country aggregation and companion items are correctly mapped."""
|
||||
domain_data = DomainBansByCountry(
|
||||
countries={"US": 10, "DE": 5, "GB": 3},
|
||||
country_names={"US": "United States", "DE": "Germany", "GB": "United Kingdom"},
|
||||
items=[
|
||||
DomainDashboardBanItem(
|
||||
ip="1.1.1.1",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
ban_count=1,
|
||||
origin="selfblock",
|
||||
),
|
||||
],
|
||||
total=18,
|
||||
)
|
||||
|
||||
result = map_domain_bans_by_country_to_response(domain_data)
|
||||
|
||||
assert result.countries == {"US": 10, "DE": 5, "GB": 3}
|
||||
assert result.country_names == {"US": "United States", "DE": "Germany", "GB": "United Kingdom"}
|
||||
assert result.total == 18
|
||||
assert len(result.bans) == 1
|
||||
|
||||
|
||||
class TestBanTrendMapper:
|
||||
"""Test mapping from DomainBanTrend to BanTrendResponse."""
|
||||
|
||||
def test_maps_buckets_and_size_label(self) -> None:
|
||||
"""Buckets and size label are correctly mapped."""
|
||||
domain_trend = DomainBanTrend(
|
||||
buckets=[
|
||||
DomainBanTrendBucket(timestamp="2026-04-28T00:00:00+00:00", count=10),
|
||||
DomainBanTrendBucket(timestamp="2026-04-28T01:00:00+00:00", count=15),
|
||||
],
|
||||
bucket_size="1h",
|
||||
)
|
||||
|
||||
result = map_domain_ban_trend_to_response(domain_trend)
|
||||
|
||||
assert result.bucket_size == "1h"
|
||||
assert len(result.buckets) == 2
|
||||
assert result.buckets[0].timestamp == "2026-04-28T00:00:00+00:00"
|
||||
assert result.buckets[0].count == 10
|
||||
|
||||
|
||||
class TestBansByJailMapper:
|
||||
"""Test mapping from DomainBansByJail to BansByJailResponse."""
|
||||
|
||||
def test_maps_jail_counts(self) -> None:
|
||||
"""Jail counts are correctly mapped."""
|
||||
domain_data = DomainBansByJail(
|
||||
jails=[
|
||||
DomainJailBanCount(jail="sshd", count=50),
|
||||
DomainJailBanCount(jail="httpd", count=20),
|
||||
],
|
||||
total=70,
|
||||
)
|
||||
|
||||
result = map_domain_bans_by_jail_to_response(domain_data)
|
||||
|
||||
assert result.total == 70
|
||||
assert len(result.jails) == 2
|
||||
assert result.jails[0].jail == "sshd"
|
||||
assert result.jails[0].count == 50
|
||||
Reference in New Issue
Block a user