197 lines
7.2 KiB
Python
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
|