Introduce service/repository dependency protocols and tests

This commit is contained in:
2026-04-10 19:51:19 +02:00
parent 3b6e39ddad
commit 3371ff8324
8 changed files with 419 additions and 11 deletions

View File

@@ -0,0 +1,189 @@
"""Router dependency injection tests.
These tests verify that routers can consume service abstractions via FastAPI
dependencies and that those dependencies can be overridden cleanly for unit
testing without touching concrete implementations.
"""
from __future__ import annotations
from pathlib import Path
import aiosqlite
from httpx import ASGITransport, AsyncClient
from app.config import Settings
from app.db import init_db
from app.dependencies import get_auth_service, get_jail_service
from app.main import create_app
from app.models.auth import Session
from app.models.jail import JailListResponse
from app.utils.setup_state import set_setup_complete_cache
class FakeAuthService:
async def login(
self,
_db: aiosqlite.Connection,
password: str,
session_duration_minutes: int,
session_repo: object | None = None,
) -> Session:
return Session(
id=1,
token="fake-token",
created_at="2025-01-01T00:00:00Z",
expires_at="2099-01-01T00:00:00Z",
)
async def validate_session(
self,
_db: aiosqlite.Connection,
token: str,
session_secret: str | None = None,
session_repo: object | None = None,
) -> Session:
return Session(
id=1,
token=token,
created_at="2025-01-01T00:00:00Z",
expires_at="2099-01-01T00:00:00Z",
)
async def logout(
self,
_db: aiosqlite.Connection,
token: str,
session_secret: str | None = None,
session_repo: object | None = None,
) -> str | None:
return token
class FakeJailService:
async def list_jails(self, _socket_path: str) -> JailListResponse:
return JailListResponse(jails=[], total=0)
async def get_jail(self, _socket_path: str, _name: str) -> JailListResponse:
raise NotImplementedError
async def reload_all(self, _socket_path: str) -> None:
raise NotImplementedError
async def start_jail(self, _socket_path: str, _name: str) -> None:
raise NotImplementedError
async def stop_jail(self, _socket_path: str, _name: str) -> None:
raise NotImplementedError
async def set_idle(self, socket_path: str, name: str, *, on: bool) -> None:
raise NotImplementedError
async def reload_jail(self, socket_path: str, name: str) -> None:
raise NotImplementedError
async def get_ignore_list(self, socket_path: str, name: str) -> list[str]:
raise NotImplementedError
async def add_ignore_ip(self, socket_path: str, name: str, ip: str) -> None:
raise NotImplementedError
async def del_ignore_ip(self, socket_path: str, name: str, ip: str) -> None:
raise NotImplementedError
async def set_ignore_self(self, socket_path: str, name: str, *, on: bool) -> None:
raise NotImplementedError
async def get_jail_banned_ips(
self,
socket_path: str,
jail_name: str,
page: int,
page_size: int,
search: str | None = None,
*,
geo_batch_lookup: object,
http_session: object,
app_db: aiosqlite.Connection,
) -> JailListResponse:
raise NotImplementedError
async def _build_app(settings: Settings):
app = create_app(settings=settings)
set_setup_complete_cache(app, True)
db = await aiosqlite.connect(settings.database_path)
db.row_factory = aiosqlite.Row
await init_db(db)
return app, db
async def test_auth_login_uses_injected_auth_service(tmp_path: Path) -> None:
settings = Settings(
database_path=str(tmp_path / "test_bangui.db"),
fail2ban_socket="/tmp/fake_fail2ban.sock",
fail2ban_config_dir=str(tmp_path / "fail2ban"),
session_secret="test-secret-key-do-not-use-in-production",
session_duration_minutes=60,
timezone="UTC",
log_level="debug",
)
app, db = await _build_app(settings)
def _fake_auth_service() -> FakeAuthService:
return FakeAuthService()
app.dependency_overrides[get_auth_service] = _fake_auth_service
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
) as client:
response = await client.post(
"/api/auth/login",
json={"password": "ignored"},
)
await db.close()
assert response.status_code == 200
assert response.json()["token"].startswith("fake-token")
assert response.cookies.get("bangui_session") is not None
async def test_jail_list_uses_injected_jail_service_and_auth(tmp_path: Path) -> None:
settings = Settings(
database_path=str(tmp_path / "test_bangui.db"),
fail2ban_socket="/tmp/fake_fail2ban.sock",
fail2ban_config_dir=str(tmp_path / "fail2ban"),
session_secret="test-secret-key-do-not-use-in-production",
session_duration_minutes=60,
timezone="UTC",
log_level="debug",
)
app, db = await _build_app(settings)
def _fake_auth_service() -> FakeAuthService:
return FakeAuthService()
def _fake_jail_service() -> FakeJailService:
return FakeJailService()
app.dependency_overrides[get_auth_service] = _fake_auth_service
app.dependency_overrides[get_jail_service] = _fake_jail_service
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
) as client:
response = await client.get(
"/api/jails",
headers={"Cookie": "bangui_session=fake-token"},
)
await db.close()
assert response.status_code == 200
assert response.json() == {"jails": [], "total": 0}