"""Handle validation rules (SPEC §4).""" from __future__ import annotations import pytest from app.config import get_settings from app.identity.handles import InvalidHandle, full_handle, validate_localpart from app.identity.reserved import is_reserved from app.identity.service import resolve_handle _DOMAIN = get_settings().app_domain @pytest.mark.parametrize( "raw,expected", [ ("alice", "alice"), ("Alice", "alice"), # case-insensitive (" bob ", "bob"), # trimmed ("a.b_c-d", "a.b_c-d"), ("user123", "user123"), ("abc", "abc"), # min length ("a" * 30, "a" * 30), # max length ], ) def test_valid_localparts(raw: str, expected: str) -> None: assert validate_localpart(raw) == expected @pytest.mark.parametrize( "raw,code", [ ("ab", "length"), # too short ("a" * 31, "length"), # too long (".alice", "dot_edge"), ("alice.", "dot_edge"), ("al..ice", "double_dot"), ("al ice", "charset"), # space ("aliçe", "charset"), # non-ascii ("al@ice", "charset"), # '@' ("UPPER!", "charset"), ("postmaster", "reserved"), ("admin", "reserved"), ("root", "reserved"), ("NoReply", "reserved"), # reserved, case-insensitive ], ) def test_invalid_localparts(raw: str, code: str) -> None: with pytest.raises(InvalidHandle) as exc: validate_localpart(raw) assert exc.value.code == code def test_full_handle_appends_domain() -> None: assert full_handle("alice") == f"alice@{_DOMAIN}" def test_reserved_is_case_insensitive() -> None: assert is_reserved("ADMIN") assert is_reserved("admin") assert not is_reserved("alice") def test_resolve_handle_accepts_localpart_or_full() -> None: assert resolve_handle("Alice") == f"alice@{_DOMAIN}" assert resolve_handle("alice@other.example") == "alice@other.example"