"""Wie lange eine Anmeldung gilt – einstellbar, bis hin zu unbegrenzt. Vorher war die Frist fest verdrahtet und lief ab dem *Anmelden*: Wer die App taeglich benutzte, flog trotzdem punktgenau nach Ablauf hinaus. Beides ist hier geregelt – die Dauer ueber eine Einstellung, das Weiterzaehlen ueber /auth/refresh. """ from datetime import datetime, timedelta, timezone import pytest from fastapi import HTTPException from jose import jwt from app.config import get_settings from app.models import ApiToken, Role, Setting, User from app.routers.auth import refresh from app.routers.settings import ( SESSION_DAYS_KEY, SESSION_DAYS_MAX, get_session_days, get_session_minutes, set_setting, ) from app.security import create_access_token, hash_api_token, hash_password @pytest.fixture def eva(db) -> User: nutzer = User(username="eva", password_hash=hash_password("geheim123"), role=Role.admin) db.add(nutzer) db.commit() db.refresh(nutzer) return nutzer def _inhalt(token: str) -> dict: e = get_settings() # Ohne Ablaufpruefung, damit auch ein abgelaufenes Token lesbar bleibt. return jwt.decode( token, e.jwt_secret, algorithms=[e.jwt_algorithm], options={"verify_exp": False}, ) # ---------------------------------------------------------------- Einstellung def test_ohne_eintrag_gilt_der_wert_aus_der_umgebung(db): erwartet = round(get_settings().jwt_expire_minutes / (60 * 24)) assert get_session_days(db) == erwartet def test_gesetzte_dauer_landet_im_token(db, eva): set_setting(key=SESSION_DAYS_KEY, value="60", db=db, _=eva) assert get_session_days(db) == 60 assert get_session_minutes(db) == 60 * 24 * 60 inhalt = _inhalt( create_access_token(eva.id, eva.role.value, gueltig_minuten=get_session_minutes(db)) ) tage = (datetime.fromtimestamp(inhalt["exp"], timezone.utc) - datetime.now(timezone.utc)).days # 59, weil der angebrochene Tag abgeschnitten wird. assert tage == 59 def test_null_heisst_unbegrenzt(db, eva): """Die Zusage "einfach fuer immer" – ein Token ganz ohne Ablauf.""" set_setting(key=SESSION_DAYS_KEY, value="0", db=db, _=eva) assert get_session_minutes(db) == 0 inhalt = _inhalt(create_access_token(eva.id, eva.role.value, gueltig_minuten=0)) assert "exp" not in inhalt def test_unbegrenztes_token_wird_angenommen(db, eva): """Ein Token ohne exp darf nicht an der Pruefung scheitern.""" from app.deps import get_current_user token = create_access_token(eva.id, eva.role.value, gueltig_minuten=0) assert get_current_user(token=token, db=db).id == eva.id @pytest.mark.parametrize("wert", ["viel", "-1", str(SESSION_DAYS_MAX + 1), "3.5"]) def test_unsinnige_dauer_wird_abgelehnt(db, eva, wert): """Absagen statt still auf die Vorgabe zurueckdrehen – sonst glaubt der Admin, es sei gesetzt.""" with pytest.raises(HTTPException) as fehler: set_setting(key=SESSION_DAYS_KEY, value=wert, db=db, _=eva) assert fehler.value.status_code == 400 assert db.get(Setting, SESSION_DAYS_KEY) is None def test_beschaedigter_eintrag_faellt_auf_die_vorgabe_zurueck(db): """Von Hand in der Datenbank verbogen: lieber die Vorgabe als ein Absturz.""" db.add(Setting(key=SESSION_DAYS_KEY, value="kaputt")) db.commit() assert get_session_days(db) == round(get_settings().jwt_expire_minutes / (60 * 24)) def test_dauer_steht_in_der_einstellungsliste(db, eva): from app.routers.settings import list_settings schluessel = {s.key for s in list_settings(db=db, _=eva)} assert SESSION_DAYS_KEY in schluessel # ---------------------------------------------------------------- Verlaengern def test_refresh_gibt_ein_frisches_token(db, eva): set_setting(key=SESSION_DAYS_KEY, value="30", db=db, _=eva) alt = create_access_token(eva.id, eva.role.value, gueltig_minuten=60) neu = refresh(token=alt, db=db) assert neu.username == "eva" assert _inhalt(neu.access_token)["exp"] > _inhalt(alt)["exp"] def test_refresh_uebernimmt_die_neue_dauer(db, eva): """Der Sinn der Sache: eine Aenderung greift, ohne dass man sich abmeldet.""" alt = create_access_token(eva.id, eva.role.value, gueltig_minuten=60) set_setting(key=SESSION_DAYS_KEY, value="0", db=db, _=eva) assert "exp" not in _inhalt(refresh(token=alt, db=db).access_token) def test_refresh_lehnt_ein_abgelaufenes_token_ab(db, eva): # Von Hand gebaut: ueber create_access_token laesst sich kein abgelaufenes # Token bestellen – alles <= 0 heisst dort "unbegrenzt". e = get_settings() abgelaufen = jwt.encode( {"sub": str(eva.id), "uid": eva.id, "role": eva.role.value, "exp": datetime.now(timezone.utc) - timedelta(minutes=1)}, e.jwt_secret, algorithm=e.jwt_algorithm, ) with pytest.raises(HTTPException) as fehler: refresh(token=abgelaufen, db=db) assert fehler.value.status_code == 401 def test_refresh_macht_aus_einem_api_token_kein_jwt(db, eva): """Ein API-Token (Home Assistant) ist jederzeit widerrufbar. Ein daraus gemachtes JWT waere es nicht – deshalb hier eine Absage.""" db.add(ApiToken(name="ha", token_hash=hash_api_token("geheim"), user_id=eva.id)) db.commit() with pytest.raises(HTTPException) as fehler: refresh(token="geheim", db=db) assert fehler.value.status_code == 401