from __future__ import annotations
from pathlib import Path
from app.services.scholar_parser import ParseState, parse_profile_page
from app.services.scholar_source import FetchResult
def _fixture(name: str) -> str:
path = Path("tests/fixtures/scholar") / name
return path.read_text(encoding="utf-8")
def _regression_fixture(name: str) -> str:
path = Path("tests/fixtures/scholar/regression") / name
return path.read_text(encoding="utf-8")
def test_parse_profile_page_extracts_core_fields_from_fixture() -> None:
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=amIMrIEAAAAJ",
status_code=200,
final_url="https://scholar.google.com/citations?hl=en&user=amIMrIEAAAAJ",
body=_fixture("profile_ok_amIMrIEAAAAJ.html"),
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.OK
assert parsed.state_reason == "publications_extracted"
assert parsed.profile_name == "Bangar Raju Cherukuri"
assert len(parsed.publications) >= 10
assert parsed.has_show_more_button is True
assert parsed.articles_range is not None
first = parsed.publications[0]
assert first.title
assert first.cluster_id
assert first.citation_count is not None
def test_parse_profile_page_classifies_accounts_redirect_as_blocked() -> None:
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=AAAAAAAAAAAA",
status_code=200,
final_url="https://accounts.google.com/v3/signin/identifier?continue=...",
body="
Sign in",
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.BLOCKED_OR_CAPTCHA
assert parsed.state_reason == "blocked_accounts_redirect"
assert len(parsed.publications) == 0
def test_parse_profile_page_handles_missing_optional_metadata() -> None:
html = """
Test Author
Articles 1-1
"""
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
status_code=200,
final_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
body=html,
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.OK
assert parsed.state_reason == "publications_extracted"
assert len(parsed.publications) == 1
publication = parsed.publications[0]
assert publication.year is None
assert publication.venue_text is None
def test_parse_profile_page_detects_layout_change_when_markers_absent() -> None:
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
status_code=200,
final_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
body="Unexpected page
",
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.LAYOUT_CHANGED
assert parsed.state_reason == "layout_markers_missing"
assert "no_rows_detected" in parsed.warnings
def test_parse_profile_page_reports_network_reason_when_status_missing() -> None:
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
status_code=None,
final_url=None,
body="",
error="timed out",
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.NETWORK_ERROR
assert parsed.state_reason == "network_error_missing_status_code"
def test_parse_profile_page_ignores_no_results_keyword_inside_script_blocks() -> None:
html = """
Scripted Author
"""
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
status_code=200,
final_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
body=html,
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.OK
assert parsed.state_reason == "no_rows_with_known_markers"
def test_parse_profile_page_treats_disabled_show_more_button_as_absent() -> None:
html = """
Disabled Show More
Articles 1-1
"""
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
status_code=200,
final_url="https://scholar.google.com/citations?hl=en&user=abcDEF123456",
body=html,
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.OK
assert parsed.has_show_more_button is False
def test_parse_profile_page_regression_fixture_profile_p1rwlvo() -> None:
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=P1RwlvoAAAAJ",
status_code=200,
final_url="https://scholar.google.com/citations?hl=en&user=P1RwlvoAAAAJ",
body=_regression_fixture("profile_P1RwlvoAAAAJ.html"),
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.OK
assert parsed.state_reason == "publications_extracted"
assert parsed.profile_name == "WENRUI ZUO"
assert len(parsed.publications) == 5
assert parsed.has_show_more_button is False
assert parsed.articles_range in {"Articles 1-5", "Articles 1–5"}
assert "possible_partial_page_show_more_present" not in parsed.warnings
assert all(item.cluster_id for item in parsed.publications)
def test_parse_profile_page_regression_fixture_profile_lz5d() -> None:
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=LZ5D_p4AAAAJ",
status_code=200,
final_url="https://scholar.google.com/citations?hl=en&user=LZ5D_p4AAAAJ",
body=_regression_fixture("profile_LZ5D_p4AAAAJ.html"),
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.OK
assert parsed.state_reason == "publications_extracted"
assert parsed.profile_name == "Doaa Elmatary"
assert len(parsed.publications) == 12
assert parsed.has_show_more_button is False
assert parsed.articles_range in {"Articles 1-12", "Articles 1–12"}
assert "possible_partial_page_show_more_present" not in parsed.warnings
assert any(item.venue_text is None for item in parsed.publications)
def test_parse_profile_page_regression_fixture_blocked_redirect() -> None:
fetch_result = FetchResult(
requested_url="https://scholar.google.com/citations?hl=en&user=AAAAAAAAAAAA",
status_code=200,
final_url=(
"https://accounts.google.com/v3/signin/identifier"
"?continue=https%3A%2F%2Fscholar.google.com%2Fcitations%3Fhl%3Den%26user%3DAAAAAAAAAAAA"
),
body=_regression_fixture("profile_AAAAAAAAAAAA.html"),
error=None,
)
parsed = parse_profile_page(fetch_result)
assert parsed.state == ParseState.BLOCKED_OR_CAPTCHA
assert parsed.state_reason == "blocked_accounts_redirect"
assert parsed.profile_name is None
assert len(parsed.publications) == 0