370 lines
12 KiB
Python
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": "<p>This bill does things.</p>"}
|
|
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": "<p>Bill summary text.</p>"}]},
|
|
{"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
|