This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
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
|
||||
Reference in New Issue
Block a user