"""Eigene Passwörter: Einmal-Passwort, Pflichtwechsel, Sperre, Zurücksetzen (→ Konzept 5.1).""" import re import pytest from fastapi.testclient import TestClient from sqlalchemy import select from app.identity import passwords from app.identity.models import User from helpers import Browser @pytest.fixture def admin(browser, people, db): with db.session() as session: session.get(User, people["anna"]).is_admin = True return browser("anna") def issued(html: str) -> str: return re.search(r'([^<]+)', html).group(1) def client_for(app) -> TestClient: client = TestClient(app) client.__enter__() return client def login(client: TestClient, username: str, password: str): return client.post("/login", data={"username": username, "password": password}, follow_redirects=False) def csrf(client: TestClient) -> str: return re.search(r'name="_csrf" value="([^"]+)"', client.get("/password").text).group(1) def test_anlegen_erstanmeldung_und_wechsel(admin, app, db): response = admin.post("/admin/users", {"username": "erik", "display_name": "Erik"}) secret = issued(response.text) with db.session() as session: user = session.scalar(select(User).where(User.username == "erik")) assert user.must_change_password and secret not in user.password_hash client = client_for(app) assert login(client, "erik", secret).status_code == 303 # Bis zum Wechsel führt jede Seite zur Passwortseite assert client.get("/workspaces", follow_redirects=False).headers["location"] == "/password" token = csrf(client) weak = client.post("/password", data={"_csrf": token, "current": secret, "new": "kurz", "repeat": "kurz"}) assert weak.status_code == 400 and "12 Zeichen" in weak.text response = client.post("/password", data={"_csrf": token, "current": secret, "new": "Sonnige Tage am See 7", "repeat": "Sonnige Tage am See 7"}, follow_redirects=False) assert response.status_code == 303 assert client.get("/workspaces").status_code == 200 # Einmal-Passwort gilt nicht mehr, das eigene schon other = client_for(app) assert login(other, "erik", secret).status_code == 401 assert login(other, "erik", "Sonnige Tage am See 7").status_code == 303 def test_demo_passwort_nur_ohne_eigenes(admin, app, db, people): from helpers import PASSWORD secret = issued(admin.post(f"/admin/users/{people['ben']}/reset-password").text) client = client_for(app) assert login(client, "ben", PASSWORD).status_code == 401 assert login(client, "ben", secret).status_code == 303 def test_ohne_demo_passwort_nur_eigene(settings, db, people): from app.main import create_app app = create_app(settings.model_copy(update={"demo_password": ""})) with db.session() as session: session.get(User, people["ben"]).password_hash = passwords.hash_password("Ein langes Passwort 1") client = client_for(app) assert login(client, "anna", "").status_code in (401, 422) assert login(client, "ben", "Ein langes Passwort 1").status_code == 303 def test_sperre_nach_fehlversuchen(app, db, people, settings): with db.session() as session: session.get(User, people["ben"]).password_hash = passwords.hash_password("Ein langes Passwort 1") client = client_for(app) for _ in range(settings.login_max_failures): assert login(client, "ben", "falsch").status_code == 401 # Gesperrt: auch das richtige Passwort hilft nicht, die Meldung bleibt gleich response = login(client, "ben", "Ein langes Passwort 1") assert response.status_code == 401 and "Nutzername oder Passwort falsch" in response.text with db.session() as session: assert session.get(User, people["ben"]).locked_until is not None def test_zuruecksetzen_entsperrt_und_beendet_sitzungen(admin, browser, db, people): ben = browser("ben") with db.session() as session: from datetime import timedelta from app.core.database import now session.get(User, people["ben"]).locked_until = now() + timedelta(minutes=10) secret = issued(admin.post(f"/admin/users/{people['ben']}/reset-password").text) assert ben.get("/workspaces", follow_redirects=False).status_code == 303 assert login(ben.client, "ben", secret).status_code == 303 def test_passwort_aendern_beendet_andere_sitzungen(app, db, people): with db.session() as session: session.get(User, people["ben"]).password_hash = passwords.hash_password("Ein langes Passwort 1") first, second = client_for(app), client_for(app) login(first, "ben", "Ein langes Passwort 1") login(second, "ben", "Ein langes Passwort 1") wrong = first.post("/password", data={"_csrf": csrf(first), "current": "falsch", "new": "Neues langes Passwort 2", "repeat": "Neues langes Passwort 2"}) assert wrong.status_code == 400 and "stimmt nicht" in wrong.text first.post("/password", data={"_csrf": csrf(first), "current": "Ein langes Passwort 1", "new": "Neues langes Passwort 2", "repeat": "Neues langes Passwort 2"}) assert first.get("/workspaces").status_code == 200 assert second.get("/workspaces", follow_redirects=False).status_code == 303 @pytest.mark.parametrize("password", ["kurz", "benbenbenbenben1", "aaaaaaaaaaaaaaa"]) def test_passwortregeln(password): from app.core.errors import Invalid with pytest.raises(Invalid): passwords.check_policy(password, "ben") def test_einmal_passwoerter_sind_verschieden_und_lang(): values = {passwords.one_time() for _ in range(50)} assert len(values) == 50 and all(len(v) == 19 for v in values) def test_create_admin_cli(settings, db, people, capsys, app): from app.cli import create_admin_user create_admin_user(settings, "root") secret = capsys.readouterr().out.rsplit(": ", 1)[1].strip() client = client_for(app) assert login(client, "root", secret).status_code == 303 assert client.get("/admin/users", follow_redirects=False).headers["location"] == "/password" def test_nicht_admin_kann_nicht_zuruecksetzen(browser, people): assert browser("ben").post(f"/admin/users/{people['clara']}/reset-password").status_code == 403 __all__ = ["Browser"]