voting-app/tests/unit/test_api_client.py
2026-05-15 15:16:18 -05:00

370 lines
12 KiB
Python

"""Test Congress.gov v3 API client mapping functions."""
from __future__ import annotations
from datetime import date
from unittest.mock import MagicMock, patch
import pytest
from src.api_client import ApiClient, get_current_congress
@pytest.fixture
def api_client() -> ApiClient:
return ApiClient("https://api.congress.gov", "test-key")
# --- Congress number calculation ---
def test_get_current_congress_2025():
"""Congress 119 started Jan 3, 2025."""
assert get_current_congress() == 1 + (2025 - 1789) // 2
def test_get_current_congress_2026():
"""Congress 120 started Jan 3, 2026."""
assert get_current_congress() == 1 + (2026 - 1789) // 2
# --- Name parsing ---
def test_parse_name_comma_format():
first, last, full = ApiClient._parse_name("McConnell, Mitch")
assert first == "Mitch"
assert last == "McConnell"
assert full == "Mitch McConnell"
def test_parse_name_no_comma():
first, last, full = ApiClient._parse_name("Van De Graaff")
assert first == ""
assert last == "Van De Graaff"
assert full == "Van De Graaff"
# --- Member list mapping ---
def test_map_member_list_basic(api_client: ApiClient):
"""Test mapping a standard member list item."""
item = {
"bioguideId": "M000355",
"name": "McConnell, Mitch",
"partyName": "Republican",
"state": "KY",
"chamber": "Senate",
"terms": {"item": [{"chamber": "Senate", "startYear": "2025", "endYear": "2027"}]},
"depiction": {"imageUrl": "https://example.com/photo.jpg"},
"url": "https://www.congress.gov/member/M000355",
}
leg = api_client._map_member_list(item, 119)
assert leg.id == "M000355"
assert leg.first_name == "Mitch"
assert leg.last_name == "McConnell"
assert leg.full_name == "Mitch McConnell"
assert leg.party == "Republican"
assert leg.state == "KY"
assert leg.chamber == "Senate"
assert leg.in_office is True
assert leg.end_date == date(2027, 1, 3)
assert leg.start_date == date(2025, 1, 3)
assert leg.photo_url == "https://example.com/photo.jpg"
def test_map_member_list_in_office(api_client: ApiClient):
"""No endYear with startYear <= current year means in-office."""
item = {
"bioguideId": "S001234",
"name": "Tester, Jeff",
"partyName": "Democrat",
"state": "ND",
"terms": {"item": [{"chamber": "Senate", "startYear": "2025"}]},
"depiction": {},
}
leg = api_client._map_member_list(item, 119)
assert leg.in_office is True
assert leg.end_date is None
assert leg.start_date == date(2025, 1, 3)
def test_map_member_list_stale_term(api_client: ApiClient):
"""Term with no endYear and startYear <= current year is in-office."""
item = {
"bioguideId": "X000001",
"name": "Old, Senator",
"partyName": "Democrat",
"state": "CA",
"terms": {"item": [{"chamber": "Senate", "startYear": "2023"}]},
"depiction": {},
}
leg = api_client._map_member_list(item, 119)
assert leg.in_office is True
def test_map_member_list_future_term(api_client: ApiClient):
"""Term with startYear in the future is not in-office."""
item = {
"bioguideId": "F000001",
"name": "Future, Member",
"partyName": "Democrat",
"state": "CA",
"terms": {"item": [{"chamber": "Senate", "startYear": str(date.today().year + 5)}]},
"depiction": {},
}
leg = api_client._map_member_list(item, 119)
assert leg.in_office is False
def test_map_member_list_fallback_photo(api_client: ApiClient):
"""Uses congress.gov photo URL when depiction is empty."""
item = {
"bioguideId": "M000355",
"name": "McConnell, Mitch",
"partyName": "Republican",
"state": "KY",
"terms": {"item": [{"chamber": "Senate", "startYear": "2025", "endYear": "2027"}]},
"depiction": {},
}
leg = api_client._map_member_list(item, 119)
assert leg.photo_url == "https://www.congress.gov/img/member/m000355_200.jpg"
# --- Member detail mapping ---
def test_map_member_detail_basic(api_client: ApiClient):
"""Test mapping a member detail response with full term history."""
item = {
"bioguideId": "M000355",
"firstName": "Mitch",
"lastName": "McConnell",
"directOrderName": "Mitch McConnell",
"partyHistory": [{"partyName": "Republican"}],
"state": "KY",
"currentMember": True,
"birthYear": "1942",
"terms": [
{"chamber": "Senate", "startYear": "1985", "endYear": "1987"},
{"chamber": "Senate", "startYear": "1987", "endYear": "2025"},
],
"depiction": {"imageUrl": "https://example.com/photo.jpg"},
"officialWebsiteUrl": "https://www.mcconnell.senate.gov",
}
leg = api_client._map_member_detail(item)
assert leg.id == "M000355"
assert leg.first_name == "Mitch"
assert leg.last_name == "McConnell"
assert leg.full_name == "Mitch McConnell"
assert leg.party == "Republican"
assert leg.state == "KY"
assert leg.chamber == "Senate"
assert leg.in_office is False
assert leg.end_date == date(2025, 1, 3)
assert leg.start_date == date(1985, 1, 3)
assert leg.first_elected == date(1985, 1, 3)
assert leg.birth_date == date(1942, 1, 1)
def test_map_member_detail_house(api_client: ApiClient):
"""Test mapping a House member."""
item = {
"bioguideId": "S000522",
"firstName": "Nancy",
"lastName": "Pelosi",
"directOrderName": "Nancy Pelosi",
"partyHistory": [{"partyName": "Democrat"}],
"state": "CA",
"currentMember": False,
"terms": [
{"chamber": "House", "startYear": "1989", "endYear": "2023"},
],
"depiction": {},
}
leg = api_client._map_member_detail(item)
assert leg.chamber == "House"
assert leg.in_office is False
assert leg.end_date == date(2023, 1, 3)
assert leg.start_date == date(1989, 1, 3)
# --- Bill mapping ---
def test_map_bill(api_client: ApiClient):
"""Test mapping a bill response."""
item = {
"congress": "119",
"type": "HR",
"number": "1",
"title": "Test Bill",
"sponsors": [{"fullName": "John Doe"}],
"latestAction": {"text": "Became Public Law 119-1"},
"policyArea": {"name": "Education"},
"legislationUrl": "https://www.congress.gov/bill/119/HR/1",
}
bill = api_client._map_bill(item)
assert bill.bill_id == "119/HR/1"
assert bill.title == "Test Bill"
assert bill.sponsor == "John Doe"
assert bill.enacted is True
assert bill.subject == "Education"
# --- Bill summary mapping ---
def test_map_bill_summary(api_client: ApiClient):
"""Test mapping a bill summary response."""
item = {"text": "&lt;p&gt;This bill does things.&lt;/p&gt;"}
summary = api_client._map_bill_summary(item)
assert summary.summary_text == "<p>This bill does things.</p>"
assert summary.model_name == "congress.gov"
# --- get_legislator endpoint ---
@patch.object(ApiClient, "_request")
def test_get_legislator(mock_request: MagicMock, api_client: ApiClient):
"""Test fetching a legislator by bioguideId."""
mock_request.return_value = {
"member": {
"bioguideId": "M000355",
"firstName": "Mitch",
"lastName": "McConnell",
"directOrderName": "Mitch McConnell",
"partyHistory": [{"partyName": "Republican"}],
"state": "KY",
"currentMember": True,
"birthYear": "1942",
"terms": [
{"chamber": "Senate", "startYear": "1985", "endYear": "1987"},
{"chamber": "Senate", "startYear": "1987", "endYear": "2025"},
],
"depiction": {"imageUrl": "https://example.com/photo.jpg"},
"officialWebsiteUrl": "https://www.mcconnell.senate.gov",
}
}
leg = api_client.get_legislator("M000355")
assert leg is not None
assert leg.id == "M000355"
assert leg.full_name == "Mitch McConnell"
assert leg.in_office is False
assert leg.end_date == date(2025, 1, 3)
@patch.object(ApiClient, "_request")
def test_get_legislator_not_found(mock_request: MagicMock, api_client: ApiClient):
"""Returns None when API returns no member data."""
mock_request.return_value = {}
assert api_client.get_legislator("MISSING") is None
# --- search_legislators endpoint ---
@patch.object(ApiClient, "_request")
def test_search_legislators(mock_request: MagicMock, api_client: ApiClient):
"""Test searching legislators with paginated Congress.gov v3 response."""
mock_request.return_value = {
"members": [
{
"bioguideId": "M000355",
"name": "McConnell, Mitch",
"partyName": "Republican",
"state": "KY",
"terms": {"item": [{"chamber": "Senate", "startYear": "2025", "endYear": "2027"}]},
"depiction": {},
},
{
"bioguideId": "S001234",
"name": "Tester, Jeff",
"partyName": "Democrat",
"state": "ND",
"terms": {"item": [{"chamber": "Senate", "startYear": "2025"}]},
"depiction": {},
},
],
"pagination": {"next": None},
}
results = api_client.search_legislators("mitch")
assert len(results) == 1
assert results[0].last_name == "McConnell"
results = api_client.search_legislators("tester")
assert len(results) == 1
assert results[0].last_name == "Tester"
@patch.object(ApiClient, "_request")
def test_search_legislators_no_results(mock_request: MagicMock, api_client: ApiClient):
"""Returns empty list when no legislators match."""
mock_request.return_value = {
"members": [
{
"bioguideId": "S001234",
"name": "Tester, Jeff",
"partyName": "Democrat",
"state": "ND",
"terms": {"item": [{"chamber": "Senate", "startYear": "2025"}]},
"depiction": {},
}
],
"pagination": {"next": None},
}
assert api_client.search_legislators("McConnell") == []
@patch.object(ApiClient, "_request")
def test_search_legislators_empty_query(mock_request: MagicMock, api_client: ApiClient):
"""Returns empty list for empty query without calling API."""
assert api_client.search_legislators("") == []
assert not mock_request.called
# --- get_bill endpoint ---
@patch.object(ApiClient, "_request")
def test_get_bill(mock_request: MagicMock, api_client: ApiClient):
"""Test fetching bill with summaries and subjects."""
mock_request.side_effect = [
{
"bill": {
"congress": "119",
"type": "HR",
"number": "1",
"title": "Test Bill",
"sponsors": [{"fullName": "John Doe"}],
"latestAction": {"text": "Referred to committee"},
"policyArea": {"name": "Education"},
"legislationUrl": "",
}
},
{"summaries": [{"text": "&lt;p&gt;Bill summary text.&lt;/p&gt;"}]},
{"subjects": {"legislativeSubjects": [{"name": "Education"}, {"name": "Technology"}]}},
]
bill = api_client.get_bill_text("119/HR/1")
assert bill is not None
assert bill.bill_id == "119/HR/1"
assert bill.title == "Test Bill"
assert bill.text == "<p>Bill summary text.</p>"
assert bill.summary == "<p>Bill summary text.</p>"
assert bill.subject == "Education, Technology"
assert bill.enacted is False
@patch.object(ApiClient, "_request")
def test_get_bill_not_found(mock_request: MagicMock, api_client: ApiClient):
"""Returns None for non-existent bill."""
mock_request.return_value = {}
assert api_client.get_bill_text("999/HR/999") is None
def test_get_bill_invalid_id(api_client: ApiClient):
"""Returns None for malformed bill ID."""
assert api_client.get_bill_text("invalid-id") is None