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: Keine 2FA" 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