import re from fastapi.testclient import TestClient from sqlalchemy import select from sqlalchemy.orm import Session from app.config import Settings from app.database import get_engine from app.main import create_app from app.models import EmailServerConfig, User def _extract_csrf(html: str) -> str: match = re.search(r'name="csrf_token" value="([^"]+)"', html) assert match is not None return match.group(1) def _build_settings(db_url: str) -> 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, EMAIL_VERIFICATION_REQUIRED=True, ) def test_register_requires_email_verification_with_mail_server(tmp_path, monkeypatch): db_path = tmp_path / "verify.db" app = create_app(_build_settings(f"sqlite:///{db_path}")) sent_mails: list[dict[str, str]] = [] def fake_send_email(*, settings, to_email: str, subject: str, text_body: str) -> None: sent_mails.append({"to": to_email, "subject": subject, "body": text_body}) monkeypatch.setattr("app.main.send_email", fake_send_email) with Session(get_engine()) as db: db.add( EmailServerConfig( smtp_host="smtp.test.local", smtp_port=587, from_email="noreply@test.local", from_name="Stundentracker", use_starttls=True, use_ssl=False, verify_tls=False, registration_mails_enabled=True, password_reset_mails_enabled=True, ) ) db.commit() with TestClient(app) as client: register_page = client.get("/register") csrf = _extract_csrf(register_page.text) register_submit = client.post( "/register", data={ "email": "verify-user@example.com", "password": "strongpasswordVerify1", "csrf_token": csrf, }, follow_redirects=False, ) assert register_submit.status_code == 303 assert register_submit.headers["location"] == "/login?msg=email_verification_sent" assert len(sent_mails) == 1 assert sent_mails[0]["to"] == "verify-user@example.com" login_page = client.get("/login") login_csrf = _extract_csrf(login_page.text) denied_login = client.post( "/login", data={ "email": "verify-user@example.com", "password": "strongpasswordVerify1", "csrf_token": login_csrf, }, follow_redirects=False, ) assert denied_login.status_code == 403 assert "Bitte zuerst deine E-Mail-Adresse bestätigen" in denied_login.text link_match = re.search(r"https?://[^\s]+/verify-email\?token=[^\s]+", sent_mails[0]["body"]) assert link_match is not None verify_response = client.get(link_match.group(0), follow_redirects=False) assert verify_response.status_code == 303 assert verify_response.headers["location"] == "/login?msg=email_verified" login_page_after_verify = client.get("/login") login_csrf_after_verify = _extract_csrf(login_page_after_verify.text) login_after_verify = client.post( "/login", data={ "email": "verify-user@example.com", "password": "strongpasswordVerify1", "csrf_token": login_csrf_after_verify, }, follow_redirects=False, ) assert login_after_verify.status_code == 303 assert login_after_verify.headers["location"].startswith("/dashboard") with Session(get_engine()) as db: verified_user = db.execute(select(User).where(User.email == "verify-user@example.com")).scalar_one() assert verified_user.email_verified is True