| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- """Unit tests for 2FA helper functions in mfa.py."""
- import base64
- import string
- import pytest
- from passlib.context import CryptContext
- from backend.app.api.routes.mfa import _generate_backup_codes, _generate_totp_qr_b64, _is_valid_email_shaped
- class TestBackupCodeGeneration:
- """Tests for backup code helpers."""
- def test_generates_ten_codes(self):
- plain, hashed = _generate_backup_codes()
- assert len(plain) == 10
- assert len(hashed) == 10
- def test_codes_are_eight_chars(self):
- plain, _ = _generate_backup_codes()
- for code in plain:
- assert len(code) == 8
- def test_codes_are_alphanumeric(self):
- allowed = set(string.ascii_uppercase + string.digits)
- plain, _ = _generate_backup_codes()
- for code in plain:
- assert all(c in allowed for c in code)
- def test_hashes_verify_against_plain(self):
- ctx = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
- plain, hashed = _generate_backup_codes()
- for p, h in zip(plain, hashed, strict=True):
- assert ctx.verify(p, h)
- def test_codes_are_unique(self):
- plain, _ = _generate_backup_codes()
- assert len(set(plain)) == 10
- class TestTOTPQRCode:
- """Tests for QR code generation helper."""
- def test_generates_base64_png(self):
- uri = "otpauth://totp/Bambuddy:testuser?secret=BASE32SECRET&issuer=Bambuddy"
- result = _generate_totp_qr_b64(uri)
- decoded = base64.b64decode(result)
- assert decoded[:4] == b"\x89PNG"
- class TestIsValidEmailShaped:
- """Direct tests for the SEC-2 email-shape check used when resolving custom
- OIDC claims like ``preferred_username`` / ``upn``. Without a dot in the
- domain or with embedded whitespace the claim is not email-shaped and
- Bambuddy must drop it — otherwise notifications / password resets would
- ship to garbage addresses."""
- @pytest.mark.parametrize(
- "value",
- [
- "alice@example.com",
- "a@b.c",
- "first.last@sub.example.co.uk",
- "user+tag@example.com",
- ],
- )
- def test_accepts_well_formed_addresses(self, value):
- assert _is_valid_email_shaped(value) is True
- @pytest.mark.parametrize(
- "value",
- [
- None,
- "",
- "a@b", # missing dot in domain
- "@example.com", # missing local part
- "alice@", # missing domain
- ".@.", # dots alone are not a domain
- "nodomain", # missing @ entirely
- "a b@example.com", # whitespace in local
- "a@exa mple.com", # whitespace in domain
- "a@b.c\n", # trailing newline — fullmatch guard
- "a\n@b.c", # embedded newline
- ],
- )
- def test_rejects_malformed_values(self, value):
- assert _is_valid_email_shaped(value) is False
- def test_rejects_pathologically_long_value(self):
- # 300-char well-shaped string — above the 255 cap.
- long_local = "a" * 260
- assert _is_valid_email_shaped(f"{long_local}@example.com") is False
|