From ee782728492c4f2f4af506f7af66edba1f400333 Mon Sep 17 00:00:00 2001 From: "Akeem T. L. King" Date: Fri, 17 Jul 2026 11:31:48 -0400 Subject: [PATCH] Expose username validation evidence --- main.py | 23 ++++++++++------ models.py | 4 +++ tests/test_validation_evidence.py | 45 +++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 tests/test_validation_evidence.py diff --git a/main.py b/main.py index f81fc9a..d7b892e 100644 --- a/main.py +++ b/main.py @@ -67,7 +67,9 @@ def get_site_list(username: str) -> list[SiteData]: ] -async def confirm_profile_exists(url: str, username: str, title: str) -> bool: +async def confirm_profile_exists( + url: str, username: str, title: str +) -> tuple[bool, XValidationResponse | None]: """ For some sites, a HEAD request may return 200 OK for both existing and non-existing profiles. In such cases, we need to perform a GET request to confirm the existence of the profile. @@ -86,18 +88,18 @@ async def confirm_profile_exists(url: str, username: str, title: str) -> bool: SSLError, ) as e: logger.warning(f"Profile validation failed for '{username}' at '{url}': {e}") - return False + return False, None except Exception as e: logger.exception( f"Unexpected error during profile validation for '{username}' at '{url}': {e}" ) - return False + return False, None if response.status_code != HTTPStatus.OK: logger.debug( f"Profile validation GET returned {response.status_code} for {url}" ) - return False + return False, None logger.info( f"Checking profile existence for '{username}' on {url} with title '{title}'" @@ -111,12 +113,16 @@ async def confirm_profile_exists(url: str, username: str, title: str) -> bool: logger.warning( f"Invalid validation response for '{username}' on '{url}': {e}" ) - return False + return False, None # if username is not available because it's taken then profile exists - return not validation.valid and validation.reason == XUsernameAvailabilityReason.taken + return ( + not validation.valid + and validation.reason == XUsernameAvailabilityReason.taken, + validation, + ) - return False + return False, None async def search_for_username(username: str) -> list[SiteResult]: """ @@ -135,7 +141,7 @@ async def run_site(site_data: SiteData): response = await client.head(validation_uri, follow_redirects=True) if response.status_code == HTTPStatus.OK: - is_profile = await confirm_profile_exists( + is_profile, validation = await confirm_profile_exists( validation_uri, username, site_data.title ) if not is_profile: @@ -149,6 +155,7 @@ async def run_site(site_data: SiteData): profile_uri=site_data.profile_uri, validation_uri=site_data.validation_uri, is_valid_profile=is_profile, + validation=validation, ) logger.debug(f"Username '{username}' found on site: {validation_uri}") return site_result diff --git a/models.py b/models.py index 5e5cd4e..b247e3f 100644 --- a/models.py +++ b/models.py @@ -24,3 +24,7 @@ class SiteData(BaseModel): class SiteResult(SiteData): is_valid_profile: bool = Field(..., description="True when the profile exists") + validation: XValidationResponse | None = Field( + default=None, + description="Public validation evidence returned by the platform endpoint", + ) diff --git a/tests/test_validation_evidence.py b/tests/test_validation_evidence.py new file mode 100644 index 0000000..1018e8b --- /dev/null +++ b/tests/test_validation_evidence.py @@ -0,0 +1,45 @@ +import asyncio +import sys +from http import HTTPStatus +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from profile_checker import main + + +class FakeResponse: + status_code = HTTPStatus.OK + + @staticmethod + def json(): + return { + "valid": False, + "reason": "taken", + "msg": "Username has already been taken", + "desc": "That username has been taken. Please choose another.", + } + + +class FakeClient: + async def get(self, *_args, **_kwargs): + return FakeResponse() + + +def test_x_validation_evidence_is_returned(monkeypatch): + monkeypatch.setattr(main, "client", FakeClient()) + + exists, evidence = asyncio.run( + main.confirm_profile_exists( + "https://api.x.com/i/users/username_available.json?username=example", + "example", + "X", + ) + ) + + assert exists is True + assert evidence.reason.value == "taken" + assert evidence.msg == "Username has already been taken" + assert evidence.desc == "That username has been taken. Please choose another."