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

@@ -20,6 +20,7 @@ if TYPE_CHECKING:
import aiosqlite
from app.models.auth import Session
from app.repositories.protocols import SessionRepository
from app.repositories import session_repo
from app.utils.constants import SESSION_TOKEN_BYTES, SESSION_TOKEN_SIGNATURE_SEPARATOR
@@ -80,6 +81,7 @@ async def login(
db: aiosqlite.Connection,
password: str,
session_duration_minutes: int,
session_repository: SessionRepository = session_repo,
) -> Session:
"""Verify *password* and create a new session on success.
@@ -108,7 +110,7 @@ async def login(
created_iso = now.isoformat()
expires_iso = add_minutes(now, session_duration_minutes).isoformat()
session = await session_repo.create_session(
session = await session_repository.create_session(
db, token=token, created_at=created_iso, expires_at=expires_iso
)
log.info("bangui_login_success", token_prefix=token[:8])
@@ -119,6 +121,7 @@ async def validate_session(
db: aiosqlite.Connection,
token: str,
session_secret: str | None = None,
session_repository: SessionRepository = session_repo,
) -> Session:
"""Return the session for *token* if it is valid and not expired.
@@ -139,13 +142,13 @@ async def validate_session(
except ValueError as exc:
raise ValueError("Session token is invalid.") from exc
session = await session_repo.get_session(db, token)
session = await session_repository.get_session(db, token)
if session is None:
raise ValueError("Session not found.")
now_iso = utc_now().isoformat()
if session.expires_at <= now_iso:
await session_repo.delete_session(db, token)
await session_repository.delete_session(db, token)
raise ValueError("Session has expired.")
return session
@@ -155,6 +158,7 @@ async def logout(
db: aiosqlite.Connection,
token: str,
session_secret: str | None = None,
session_repository: SessionRepository = session_repo,
) -> str | None:
"""Invalidate the session identified by *token*.
@@ -173,6 +177,6 @@ async def logout(
log.warning("bangui_logout_invalid_token", token_prefix=token[:8])
return None
await session_repo.delete_session(db, token)
await session_repository.delete_session(db, token)
log.info("bangui_logout", token_prefix=token[:8])
return token

View File

@@ -0,0 +1,105 @@
"""Service interface protocols for dependency injection.
These structural protocols define the public contract that routers and higher
layers depend on, without binding them to concrete module implementations.
"""
from __future__ import annotations
from typing import Protocol
import aiosqlite
from app.models.auth import Session
from app.models.ban import JailBannedIpsResponse
from app.models.jail import JailDetailResponse, JailListResponse
class AuthService(Protocol):
"""Protocol for authentication service operations."""
async def login(
self,
db: aiosqlite.Connection,
password: str,
session_duration_minutes: int,
session_repo: object | None = None,
) -> Session:
...
async def validate_session(
self,
db: aiosqlite.Connection,
token: str,
session_secret: str | None = None,
session_repo: object | None = None,
) -> Session:
...
async def logout(
self,
db: aiosqlite.Connection,
token: str,
session_secret: str | None = None,
session_repo: object | None = None,
) -> str | None:
...
class JailService(Protocol):
"""Protocol for jail management service operations."""
async def list_jails(self, socket_path: str) -> JailListResponse:
...
async def get_jail(self, socket_path: str, name: str) -> JailDetailResponse:
...
async def reload_all(self, socket_path: str) -> None:
...
async def start_jail(self, socket_path: str, name: str) -> None:
...
async def stop_jail(self, socket_path: str, name: str) -> None:
...
async def set_idle(self, socket_path: str, name: str, *, on: bool) -> None:
...
async def reload_jail(self, socket_path: str, name: str) -> None:
...
async def get_ignore_list(self, socket_path: str, name: str) -> list[str]:
...
async def add_ignore_ip(self, socket_path: str, name: str, ip: str) -> None:
...
async def del_ignore_ip(self, socket_path: str, name: str, ip: str) -> None:
...
async def set_ignore_self(self, socket_path: str, name: str, *, on: bool) -> None:
...
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,
) -> JailBannedIpsResponse:
...
async def lookup_ip(
self,
socket_path: str,
ip: str,
geo_enricher: object,
) -> object:
...