Files
Vorrania/backend/tests/test_sitzungsdauer.py
T
ScarriffleandClaude Opus 5 254737086e Sitzungsdauer einstellbar, bis hin zu unbegrenzt
Die Anmeldung galt fest 7 Tage - und die Frist lief ab dem *Anmelden*, nicht
ab der letzten Nutzung. Wer die App taeglich benutzte, flog trotzdem
punktgenau nach einer Woche hinaus.

Beides ist jetzt geregelt:

- Einstellungen -> Anmeldung: 7 Tage bis 1 Jahr oder unbegrenzt. Gilt fuer
  Website und App gemeinsam, weil beide dasselbe Token bekommen - auch nach
  Single Sign-on.
- POST /auth/refresh schiebt die Frist beim Start nach vorn. Damit zaehlt die
  eingestellte Dauer ab dem letzten Besuch, und eine geaenderte Einstellung
  greift ohne Neuanmeldung.

"Unbegrenzt" heisst ein Token ganz ohne exp. Das ist bewusst folgenreich: Es
gibt keine Sperrliste, eine erteilte Anmeldung laesst sich nicht einzeln
zurueckziehen. Die Oberflaeche sagt das dazu und nennt den einzigen Ausweg
(JWT_SECRET aendern).

/auth/refresh nimmt bewusst nur JWTs an, keine langlebigen API-Token: aus
einem jederzeit widerrufbaren Zugang (Home Assistant) waere sonst ein
unwiderruflicher geworden.

Der Rueckfallwert aus der Umgebung steigt von 7 auf 30 Tage - er gilt nur noch,
solange nichts eingestellt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 23:15:46 +02:00

152 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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