Files
stundenfuchs/tests/test_admin_and_mfa.py
T
maddin 9794362f39
CI / checks (push) Has been cancelled
chore: initialize public repository
2026-03-22 12:55:55 +00:00

197 lines
7.2 KiB
Python

from fastapi.testclient import TestClient
import pyotp
from app.config import Settings
from app.main import create_app
def _build_settings(db_url: str, *, bootstrap_admin_email: str | None = None) -> Settings:
return Settings(
APP_ENV="test",
DB_URL=db_url,
SESSION_SECRET="test-secret",
COOKIE_SECURE=False,
COOKIE_SAMESITE="lax",
LOGIN_RATE_LIMIT_ATTEMPTS=5,
LOGIN_RATE_LIMIT_WINDOW_MINUTES=15,
BOOTSTRAP_ADMIN_EMAIL=bootstrap_admin_email,
)
def test_bootstrap_admin_can_manage_users(tmp_path):
db_path = tmp_path / "admin.db"
app = create_app(_build_settings(f"sqlite:///{db_path}", bootstrap_admin_email="admin@example.com"))
with TestClient(app) as admin_client, TestClient(app) as user_client:
admin_register = admin_client.post(
"/auth/register",
json={"email": "admin@example.com", "password": "verystrongPass123"},
)
assert admin_register.status_code == 200
admin_payload = admin_register.json()
assert admin_payload["role"] == "admin"
admin_csrf = admin_payload["csrf_token"]
user_register = user_client.post(
"/auth/register",
json={"email": "normal@example.com", "password": "verystrongPass123"},
)
assert user_register.status_code == 200
user_payload = user_register.json()
user_id = user_payload["id"]
user_csrf = user_payload["csrf_token"]
forbidden = user_client.post(
"/settings/admin/users/" + user_id,
data={"csrf_token": user_csrf, "role": "admin", "is_active": "on"},
follow_redirects=False,
)
assert forbidden.status_code == 403
updated = admin_client.post(
"/settings/admin/users/" + user_id,
data={"csrf_token": admin_csrf, "role": "admin", "is_active": "on"},
follow_redirects=False,
)
assert updated.status_code == 303
assert updated.headers["location"].startswith("/settings?tab=admin&msg=admin_user_updated")
me_after = user_client.get("/me")
assert me_after.status_code == 200
assert me_after.json()["role"] == "admin"
def test_special_case_email_no_longer_becomes_admin(app):
with TestClient(app) as client:
register = client.post(
"/auth/register",
json={"email": "special-case@example.net", "password": "verystrongPass123"},
)
assert register.status_code == 200
assert register.json()["role"] == "user"
def test_totp_mfa_login_flow(app):
password = "verystrongPass123"
with TestClient(app) as client:
register = client.post(
"/auth/register",
json={"email": "mfa-user@example.com", "password": password},
)
assert register.status_code == 200
csrf_token = register.json()["csrf_token"]
setup_start = client.post(
"/settings/mfa",
data={
"csrf_token": csrf_token,
"mfa_method": "totp",
"current_password": password,
"setup_code": "",
},
)
assert setup_start.status_code == 200
assert "TOTP Secret" in setup_start.text
assert "TOTP-Einrichtung läuft" in setup_start.text
assert "Aktive Methode: <strong>Keine 2FA</strong>" not in setup_start.text
assert 'option value="totp" selected' in setup_start.text
assert 'option value="none" selected' not in setup_start.text
marker = 'name="setup_code"'
assert marker in setup_start.text
secret_prefix = 'TOTP Secret'
assert secret_prefix in setup_start.text
value_marker = 'readonly'
assert value_marker in setup_start.text
# Extract setup secret from rendered readonly input field.
snippet_start = setup_start.text.find("TOTP Secret")
secret_value_start = setup_start.text.find("value=\"", snippet_start) + len("value=\"")
secret_value_end = setup_start.text.find("\"", secret_value_start)
setup_secret = setup_start.text[secret_value_start:secret_value_end]
assert setup_secret
setup_code = pyotp.TOTP(setup_secret).now()
setup_finish = client.post(
"/settings/mfa",
data={
"csrf_token": csrf_token,
"mfa_method": "totp",
"current_password": password,
"setup_code": setup_code,
},
follow_redirects=False,
)
assert setup_finish.status_code == 303
assert setup_finish.headers["location"].startswith("/settings?msg=mfa_updated")
settings_after = client.get("/settings")
assert settings_after.status_code == 200
assert "Status:" in settings_after.text
assert "Authenticator-App (TOTP)" in settings_after.text
assert "TOTP-Einrichtung läuft" not in settings_after.text
def test_totp_setup_form_keeps_totp_selected_until_confirmation(app):
password = "verystrongPass123"
with TestClient(app) as client:
register = client.post(
"/auth/register",
json={"email": "mfa-user-2@example.com", "password": password},
)
assert register.status_code == 200
csrf_token = register.json()["csrf_token"]
setup_start = client.post(
"/settings/mfa",
data={
"csrf_token": csrf_token,
"mfa_method": "totp",
"current_password": password,
"setup_code": "",
},
)
assert setup_start.status_code == 200
assert 'option value="totp" selected' in setup_start.text
assert 'option value="none" selected' not in setup_start.text
def test_admin_can_delete_user_but_not_self(tmp_path):
db_path = tmp_path / "admin-delete.db"
app = create_app(_build_settings(f"sqlite:///{db_path}", bootstrap_admin_email="admin@example.com"))
with TestClient(app) as admin_client, TestClient(app) as user_client:
admin_register = admin_client.post(
"/auth/register",
json={"email": "admin@example.com", "password": "verystrongPass123"},
)
assert admin_register.status_code == 200
admin_payload = admin_register.json()
admin_id = admin_payload["id"]
admin_csrf = admin_payload["csrf_token"]
user_register = user_client.post(
"/auth/register",
json={"email": "delete-me@example.com", "password": "verystrongPass123"},
)
assert user_register.status_code == 200
user_id = user_register.json()["id"]
delete_user = admin_client.post(
f"/settings/admin/users/{user_id}/delete",
data={"csrf_token": admin_csrf},
follow_redirects=False,
)
assert delete_user.status_code == 303
assert delete_user.headers["location"].startswith("/settings?tab=admin&msg=admin_user_deleted")
me_deleted_user = user_client.get("/me")
assert me_deleted_user.status_code == 401
delete_self = admin_client.post(
f"/settings/admin/users/{admin_id}/delete",
data={"csrf_token": admin_csrf},
follow_redirects=False,
)
assert delete_self.status_code == 400