Convert setup guard to startup-driven cache and update tests

This commit is contained in:
2026-04-06 20:38:15 +02:00
parent 3ccfc20c64
commit 89ab41cc9e
5 changed files with 109 additions and 59 deletions

View File

@@ -12,6 +12,7 @@ from httpx import ASGITransport, AsyncClient
from app.config import Settings
from app.db import init_db
from app.main import _lifespan, create_app
from app.services import setup_service
# ---------------------------------------------------------------------------
# Shared setup payload
@@ -224,32 +225,18 @@ class TestGetTimezone:
class TestSetupCompleteCaching:
"""SetupRedirectMiddleware caches the setup_complete flag in ``app.state``."""
async def test_cache_flag_set_after_first_post_setup_request(
self,
app_and_client: tuple[object, AsyncClient],
async def test_cache_flag_set_after_post_setup(
self, app_and_client: tuple[object, AsyncClient]
) -> None:
"""``_setup_complete_cached`` is set to True on the first request after setup.
The ``/api/setup`` path is in ``_ALWAYS_ALLOWED`` so it bypasses the
middleware check. The first request to a non-exempt endpoint triggers
the DB query and, when setup is complete, populates the cache flag.
"""
"""``setup_complete_cached`` is set to True immediately after setup."""
from fastapi import FastAPI
app, client = app_and_client
assert isinstance(app, FastAPI)
# Complete setup (exempt from middleware, no flag set yet).
resp = await client.post("/api/setup", json=_SETUP_PAYLOAD)
assert resp.status_code == 201
# Flag not yet cached — setup was via an exempt path.
assert not getattr(app.state, "_setup_complete_cached", False)
# First non-exempt request — middleware queries DB and sets the flag.
await client.post("/api/auth/login", json={"password": _SETUP_PAYLOAD["master_password"]})
assert app.state._setup_complete_cached is True
assert app.state.setup_complete_cached is True
async def test_cached_path_skips_is_setup_complete(
self,
@@ -268,11 +255,11 @@ class TestSetupCompleteCaching:
# Do setup and warm the cache.
await client.post("/api/setup", json=_SETUP_PAYLOAD)
await client.post("/api/auth/login", json={"password": _SETUP_PAYLOAD["master_password"]})
assert app.state._setup_complete_cached is True
assert app.state.setup_complete_cached is True
call_count = 0
async def _counting(db: aiosqlite.Connection) -> bool:
async def _counting(_db: aiosqlite.Connection) -> bool:
nonlocal call_count
call_count += 1
return True
@@ -380,6 +367,55 @@ class TestLifespanDatabaseDirectoryCreation:
assert tmp_path.exists()
class TestLifespanSetupCache:
"""Verify that app startup resolves setup completion into app.state."""
async def test_startup_caches_setup_completion(self, tmp_path: Path) -> None:
"""Lifespan should populate ``setup_complete_cached`` based on the DB."""
settings = Settings(
database_path=str(tmp_path / "bangui.db"),
fail2ban_socket="/tmp/fake.sock",
session_secret="test-lifespan-setup-cache-secret",
session_duration_minutes=60,
timezone="UTC",
log_level="debug",
)
app = create_app(settings=settings)
db = await aiosqlite.connect(settings.database_path)
db.row_factory = aiosqlite.Row
await init_db(db)
await setup_service.run_setup(
db,
master_password="supersecret123",
database_path=settings.database_path,
fail2ban_socket=settings.fail2ban_socket,
timezone=settings.timezone,
session_duration_minutes=settings.session_duration_minutes,
)
await db.close()
mock_scheduler = MagicMock()
mock_scheduler.start = MagicMock()
mock_scheduler.shutdown = MagicMock()
with (
patch("app.services.geo_service.init_geoip"),
patch(
"app.services.geo_service.load_cache_from_db",
new=AsyncMock(return_value=None),
),
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.main.AsyncIOScheduler", return_value=mock_scheduler),
patch("app.main.ensure_jail_configs"),
):
async with _lifespan(app):
assert app.state.setup_complete_cached is True
# ---------------------------------------------------------------------------
# Task 0.2 — Middleware redirects when app.state.db is None
# ---------------------------------------------------------------------------