import pytest from pydantic import ValidationError from porchlight.validation import ( GroupListInput, PasswordChange, PasswordSet, ProfileUpdate, UsernameInput, format_validation_errors, ) class TestProfileUpdateEmail: def test_valid_email(self) -> None: p = ProfileUpdate(email="user@example.com") assert p.email == "user@example.com" def test_empty_email_becomes_none(self) -> None: p = ProfileUpdate(email="") assert p.email is None def test_invalid_email_no_at(self) -> None: with pytest.raises(ValidationError, match="email"): ProfileUpdate(email="not-an-email") def test_invalid_email_no_domain(self) -> None: with pytest.raises(ValidationError, match="email"): ProfileUpdate(email="user@") class TestProfileUpdatePhone: def test_valid_e164(self) -> None: p = ProfileUpdate(phone_number="+46701234567") assert p.phone_number == "+46701234567" def test_valid_e164_with_spaces_normalized(self) -> None: p = ProfileUpdate(phone_number="+46 70 123 45 67") assert p.phone_number == "+46701234567" def test_empty_phone_becomes_none(self) -> None: p = ProfileUpdate(phone_number="") assert p.phone_number is None def test_invalid_phone(self) -> None: with pytest.raises(ValidationError, match="phone"): ProfileUpdate(phone_number="not-a-phone") def test_invalid_phone_no_plus(self) -> None: with pytest.raises(ValidationError, match="phone"): ProfileUpdate(phone_number="46701234567") class TestProfileUpdatePicture: def test_valid_https_url(self) -> None: p = ProfileUpdate(picture="https://example.com/pic.jpg") assert p.picture == "https://example.com/pic.jpg" def test_valid_http_url(self) -> None: p = ProfileUpdate(picture="http://example.com/pic.jpg") assert p.picture == "http://example.com/pic.jpg" def test_empty_picture_becomes_none(self) -> None: p = ProfileUpdate(picture="") assert p.picture is None def test_invalid_picture_not_url(self) -> None: with pytest.raises(ValidationError, match="picture"): ProfileUpdate(picture="not-a-url") def test_invalid_picture_ftp_scheme(self) -> None: with pytest.raises(ValidationError, match="picture"): ProfileUpdate(picture="ftp://example.com/pic.jpg") class TestProfileUpdateFieldLengths: def test_given_name_too_long(self) -> None: with pytest.raises(ValidationError, match="given_name"): ProfileUpdate(given_name="x" * 256) def test_phone_number_max_length(self) -> None: """E.164 max is 15 digits + plus sign = 16 chars.""" p = ProfileUpdate(phone_number="+431234567890123") assert p.phone_number == "+431234567890123" def test_locale_too_long(self) -> None: with pytest.raises(ValidationError, match="locale"): ProfileUpdate(locale="x" * 21) class TestProfileUpdateDefaults: def test_all_defaults(self) -> None: p = ProfileUpdate() assert p.given_name == "" assert p.family_name == "" assert p.preferred_username == "" assert p.email is None assert p.phone_number is None assert p.picture is None assert p.locale == "" class TestProfileUpdateLocale: def test_valid_locale_language_only(self) -> None: p = ProfileUpdate(locale="en") assert p.locale == "en" def test_valid_locale_language_region(self) -> None: p = ProfileUpdate(locale="sv-SE") assert p.locale == "sv-SE" def test_valid_locale_language_script_region(self) -> None: p = ProfileUpdate(locale="zh-Hans-CN") assert p.locale == "zh-Hans-CN" def test_valid_locale_three_letter(self) -> None: p = ProfileUpdate(locale="gsw") assert p.locale == "gsw" def test_empty_locale_allowed(self) -> None: p = ProfileUpdate(locale="") assert p.locale == "" def test_invalid_locale_rejected(self) -> None: with pytest.raises(ValidationError, match="locale"): ProfileUpdate(locale="not_a_locale!") def test_invalid_locale_numbers(self) -> None: with pytest.raises(ValidationError, match="locale"): ProfileUpdate(locale="12345") def test_whitespace_locale_becomes_empty(self) -> None: p = ProfileUpdate(locale=" ") assert p.locale == "" class TestUsernameInput: def test_valid_simple(self) -> None: u = UsernameInput(username="alice") assert u.username == "alice" def test_valid_with_dots_dashes_underscores(self) -> None: u = UsernameInput(username="alice.bob_charlie-1") assert u.username == "alice.bob_charlie-1" def test_valid_email_style(self) -> None: u = UsernameInput(username="user@example.com") assert u.username == "user@example.com" def test_valid_uppercase(self) -> None: u = UsernameInput(username="Alice") assert u.username == "Alice" def test_empty_rejected(self) -> None: with pytest.raises(ValidationError, match="username"): UsernameInput(username="") def test_whitespace_only_rejected(self) -> None: with pytest.raises(ValidationError, match="username"): UsernameInput(username=" ") def test_too_long_rejected(self) -> None: with pytest.raises(ValidationError): UsernameInput(username="a" * 256) def test_spaces_rejected(self) -> None: with pytest.raises(ValidationError, match="username"): UsernameInput(username="alice bob") def test_special_chars_rejected(self) -> None: with pytest.raises(ValidationError, match="username"): UsernameInput(username="alice