Finish external HTTP client resilience: add shared aiohttp config, retry support, and update task status

This commit is contained in:
2026-04-09 22:01:11 +02:00
parent e1d741956e
commit 148756fb79
6 changed files with 185 additions and 21 deletions

View File

@@ -123,6 +123,57 @@ async def test_lifespan_initialises_and_cleans_up_shared_resources(tmp_path: Pat
mock_scheduler.shutdown.assert_called_once_with(wait=False)
async def test_http_session_is_created_with_configured_timeouts_and_limits(tmp_path: Path) -> None:
"""The shared HTTP client session is created with the configured limits."""
settings = Settings(
database_path=str(tmp_path / "bangui.db"),
fail2ban_socket="/tmp/fake_fail2ban.sock",
fail2ban_config_dir=str(tmp_path / "fail2ban"),
session_secret="test-lifespan-secret",
session_duration_minutes=60,
timezone="UTC",
log_level="debug",
http_request_timeout_seconds=12.5,
http_connect_timeout_seconds=1.5,
http_max_connections=5,
http_keepalive_timeout_seconds=8.0,
)
app = create_app(settings=settings)
mock_scheduler = MagicMock()
mock_scheduler.start = MagicMock()
mock_scheduler.shutdown = MagicMock()
mock_http_session = MagicMock()
mock_http_session.close = AsyncMock()
with (
patch("app.startup.ensure_jail_configs"),
patch("app.startup.aiohttp.ClientSession", return_value=mock_http_session) as mock_client_session,
patch("app.startup.AsyncIOScheduler", return_value=mock_scheduler),
patch("app.startup.init_db", new=AsyncMock()),
patch("app.services.geo_service.init_geoip"),
patch("app.services.geo_service.load_cache_from_db", new=AsyncMock(return_value=None)),
patch("app.services.geo_service.count_unresolved", new=AsyncMock(return_value=0)),
patch("app.services.setup_service.is_setup_complete", new=AsyncMock(return_value=False)),
patch("app.tasks.health_check.register"),
patch("app.tasks.blocklist_import.register"),
patch("app.tasks.geo_cache_flush.register"),
patch("app.tasks.geo_re_resolve.register"),
patch("app.tasks.history_sync.register"),
):
async with _lifespan(app):
assert mock_client_session.call_count == 1
kwargs = mock_client_session.call_args.kwargs
timeout = kwargs["timeout"]
connector = kwargs["connector"]
assert timeout.total == 12.5
assert timeout.connect == 1.5
assert timeout.sock_read == 12.5
assert connector.limit == 5
assert connector.limit_per_host == 5
async def test_startup_overrides_settings_from_persisted_setup(tmp_path: Path) -> None:
"""Startup should replace env defaults with values persisted by setup."""
env_settings = Settings(

View File

@@ -125,6 +125,27 @@ class TestPreview:
with pytest.raises(ValueError, match="HTTP 404"):
await blocklist_service.preview_source("https://bad.test/", session)
async def test_preview_retries_transient_errors(self) -> None:
"""preview_source retries transient network failures before succeeding."""
content = "1.2.3.4\n"
mock_resp = AsyncMock()
mock_resp.status = 200
mock_resp.text = AsyncMock(return_value=content)
mock_resp.content = AsyncMock()
mock_resp.content.read = AsyncMock(return_value=content.encode())
mock_ctx = AsyncMock()
mock_ctx.__aenter__.return_value = mock_resp
mock_ctx.__aexit__.return_value = False
session = MagicMock()
session.get = MagicMock(side_effect=[Exception("connection reset"), mock_ctx])
result = await blocklist_service.preview_source("https://test.test/ips.txt", session)
assert result.valid_count == 1
assert session.get.call_count == 2
async def test_preview_limits_entries(self) -> None:
"""preview_source caps entries to sample_lines."""
ips = "\n".join(f"1.2.3.{i}" for i in range(50))