bifroest/tests/test_passwords.py
2026-10-27 08:34:17 +01:00

145 lines
6.2 KiB
Python

"""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'<code class="text-lg">([^<]+)</code>', 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"]