"""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 == "
This bill does things.
" 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 == "Bill summary text.
" assert bill.summary == "Bill summary text.
" 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