"""Unit tests: single-admin auth (phase 16; A10 revised). Covers the config gate (fail-loud, including via ``create_app``), the constant-time password check, the ``require_admin`` dependency, the whoami payload shape, and the sign_in/sign_out session semantics. """ from __future__ import annotations import pytest from fastapi import HTTPException from starlette.middleware.sessions import Session from starlette.requests import Request import app.main as main_mod from app.api.auth import whoami from app.config import Settings from app.core.auth import ( check_password, ensure_admin_configured, require_admin, sign_in, sign_out, ) from app.schemas import WhoamiResponse def _settings(**kwargs: object) -> Settings: return Settings(_env_file=None, **kwargs) # pyright: ignore[reportCallIssue] # ---------- ensure_admin_configured (fail-loud) ---------- def test_configured_passes() -> None: ensure_admin_configured(_settings(admin_password="pw", session_secret="s")) @pytest.mark.parametrize( ("password", "secret", "named"), [ ("", "s", "BOR_ADMIN_PASSWORD"), ("pw", "", "BOR_SESSION_SECRET"), ("", "", "BOR_ADMIN_PASSWORD"), ("", "", "BOR_SESSION_SECRET"), ], ) def test_missing_vars_raise_naming_them(password: str, secret: str, named: str) -> None: with pytest.raises(RuntimeError) as exc: ensure_admin_configured(_settings(admin_password=password, session_secret=secret)) assert named in str(exc.value) # Both vars are named when both are missing. if not password and not secret: assert "BOR_ADMIN_PASSWORD" in str(exc.value) assert "BOR_SESSION_SECRET" in str(exc.value) def test_whitespace_only_counts_as_missing() -> None: with pytest.raises(RuntimeError, match="BOR_ADMIN_PASSWORD"): ensure_admin_configured(_settings(admin_password=" ", session_secret="s")) def test_create_app_raises_when_admin_password_missing(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(main_mod.settings, "admin_password", "") with pytest.raises(RuntimeError, match="BOR_ADMIN_PASSWORD"): main_mod.create_app() def test_create_app_raises_when_session_secret_missing(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(main_mod.settings, "session_secret", "") with pytest.raises(RuntimeError, match="BOR_SESSION_SECRET"): main_mod.create_app() def test_create_app_boots_when_configured(monkeypatch: pytest.MonkeyPatch) -> None: # conftest set both env vars before app.main imported — the factory # with valid config returns an app (and no static-dir warning fires # here: the frontend dir exists in the repo). monkeypatch.setattr(main_mod.settings, "admin_password", "pw") monkeypatch.setattr(main_mod.settings, "session_secret", "s") app2 = main_mod.create_app() assert app2 is not None # ---------- check_password (constant-time, one generic result) ---------- def test_check_password_match() -> None: assert check_password("hunter2", "hunter2") is True def test_check_password_mismatch() -> None: assert check_password("hunter2", "hunter3") is False def test_check_password_empty_candidate() -> None: assert check_password("", "hunter2") is False def test_check_password_unicode() -> None: assert check_password("pässwörd", "pässwörd") is True assert check_password("pässwörd", "pässwörX") is False # ---------- require_admin (dependency: 403 for anonymous) ---------- def _request_with_session(**session: object) -> Request: # request.session is a scope-backed property (SessionMiddleware puts # the Session into the scope) — build the scope the same way. return Request({"type": "http", "session": Session(dict(session))}) def test_require_admin_passes_for_admin_session() -> None: require_admin(_request_with_session(admin=True)) # no exception @pytest.mark.parametrize("session", [{}, {"admin": False}, {"admin": None}]) def test_require_admin_403s_anonymous(session: dict) -> None: with pytest.raises(HTTPException) as exc: require_admin(_request_with_session(**session)) assert exc.value.status_code == 403 assert exc.value.detail == "admin only" # ---------- whoami payload shape ---------- def test_whoami_anonymous_payload() -> None: body = whoami(_request_with_session()) assert body == WhoamiResponse(authenticated=False, role="anonymous") assert set(body.model_dump()) == {"authenticated", "role"} def test_whoami_admin_payload() -> None: body = whoami(_request_with_session(admin=True)) assert body == WhoamiResponse(authenticated=True, role="admin") # ---------- sign_in / sign_out session semantics ---------- def test_sign_in_marks_session_admin_and_modified() -> None: session = Session({}) sign_in(session) assert session["admin"] is True assert session.modified is True # the middleware will persist the cookie def test_sign_out_clears_session() -> None: session = Session({"admin": True, "stray": "x"}) sign_out(session) assert dict(session) == {} assert "admin" not in session