"""Login-Sperre, TOTP und Medien-Grant ohne Datenbank.""" from __future__ import annotations import os os.environ.setdefault("SKIP_DB_MIGRATE", "1") os.environ.setdefault("REQUIRE_ADMIN_TOTP", "0") import pytest from fastapi.testclient import TestClient from main import app from media_grant import issue_media_grant, profile_id_from_media_grant from totp_auth import generate_totp_secret, totp_code, verify_totp import time @pytest.fixture def client() -> TestClient: return TestClient(app) def test_totp_accepts_current_code() -> None: secret = generate_totp_secret() code = totp_code(secret, int(time.time()) // 30) assert verify_totp(secret, code) assert not verify_totp(secret, "000000") or code == "000000" def test_media_grant_rejects_session_shaped_token() -> None: assert profile_id_from_media_grant("not-a-media-grant") is None token, _exp = issue_media_grant(42, ttl_seconds=60) assert profile_id_from_media_grant(token) == 42 assert profile_id_from_media_grant(token + "x") is None def test_change_password_requires_current_password() -> None: from pydantic import ValidationError from models import ChangePasswordRequest with pytest.raises(ValidationError): ChangePasswordRequest.model_validate({"pin": "neuespasswort"}) def test_password_reset_rejects_header_injection() -> None: from pydantic import ValidationError from models import PasswordResetRequest, ResendVerificationRequest with pytest.raises(ValidationError): PasswordResetRequest.model_validate({"email": "trainer@example.com\nBcc: x@y.z"}) with pytest.raises(ValidationError): ResendVerificationRequest.model_validate({"email": "nicht-eine-email"}) def test_upload_mime_comes_from_bytes_not_client_header() -> None: from routers.exercises import _content_disposition, resolve_upload_mime_type jpeg = b"\xff\xd8\xff" + b"\x00" * 16 assert resolve_upload_mime_type(jpeg, "text/html", "evil.html") == "image/jpeg" pdf = b"%PDF-1.4\n" + b"0" * 8 assert resolve_upload_mime_type(pdf, "image/png", "bild.png") == "application/pdf" try: resolve_upload_mime_type(b"", "image/jpeg", "x.jpg") raised = False except ValueError: raised = True assert raised disposition = _content_disposition('bericht.pdf\r\nX-Evil: 1', attachment=True) assert disposition.startswith("attachment") assert "\r" not in disposition assert "\n" not in disposition def test_totp_reset_requires_auth(client: TestClient) -> None: response = client.post("/api/profiles/1/totp-reset") assert response.status_code == 401 def test_login_rejects_oversized_password(client: TestClient) -> None: response = client.post( "/api/auth/login", json={"email": "trainer@example.com", "password": "x" * 200}, ) assert response.status_code == 422