Some checks failed
Deploy Development / deploy (push) Successful in 59s
Test Suite / pytest-backend (push) Failing after 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 17s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Failing after 7m48s
A superadmin can also turn off another account''s two-factor when the authenticator is lost. Co-authored-by: Cursor <cursoragent@cursor.com>
87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""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"<html><script>alert(1)</script></html>", "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
|