"""Test Flask application routes.""" from __future__ import annotations from datetime import date from unittest.mock import MagicMock, patch import pytest from src.app import create_app from src.models import Legislator, Vote @pytest.fixture def app(): a = create_app() a.config["TESTING"] = True return a @pytest.fixture def client(app): return app.test_client() # --- Health endpoint --- def test_health(client): resp = client.get("/health") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "ok" # --- Search endpoint --- def test_search_no_query(client): resp = client.get("/api/search") assert resp.get_json() == [] def test_search_empty_query(client): resp = client.get("/api/search?q=") assert resp.get_json() == [] def test_search_with_query(app, client): mock_api = MagicMock() mock_cache = MagicMock() mock_cache.search_legislators.return_value = [] mock_api.search_legislators.return_value = [ Legislator( id="L001", first_name="John", last_name="Doe", full_name="John Doe", party="Democrat", state="CA", chamber="Senate", in_office=True, photo_url="https://example.com/photo.jpg", ) ] mock_api.save_legislator = MagicMock() mock_cache.save_legislator = MagicMock() with patch.dict(app.config, {"API": mock_api, "CACHE": mock_cache}): resp = client.get("/api/search?q=John") data = resp.get_json() assert len(data) == 1 assert data[0]["full_name"] == "John Doe" assert data[0]["in_office"] is True assert data[0]["photo_url"] == "https://example.com/photo.jpg" def test_search_api_failure_returns_empty(app, client): mock_api = MagicMock() mock_cache = MagicMock() mock_cache.search_legislators.return_value = [] mock_api.search_legislators.side_effect = Exception("Network error") with patch.dict(app.config, {"API": mock_api, "CACHE": mock_cache}): resp = client.get("/api/search?q=John") assert resp.get_json() == [] # --- Legislator detail endpoint --- def test_legislator_detail_cached(app, client): mock_cache = MagicMock() mock_api = MagicMock() mock_cache.get_legislator.return_value = Legislator( id="M000355", first_name="Mitch", last_name="McConnell", full_name="Mitch McConnell", party="Republican", state="KY", chamber="Senate", in_office=False, end_date=date(2025, 12, 31), photo_url="https://example.com/photo.jpg", url="https://www.mcconnell.senate.gov", ) with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api}): resp = client.get("/api/legislators/M000355") data = resp.get_json() assert resp.status_code == 200 assert data["full_name"] == "Mitch McConnell" assert data["in_office"] is False mock_api.get_legislator.assert_not_called() def test_legislator_detail_fetches_from_api(app, client): mock_cache = MagicMock() mock_api = MagicMock() mock_cache.get_legislator.return_value = None mock_api.get_legislator.return_value = Legislator( id="S001", first_name="Jane", last_name="Smith", full_name="Jane Smith", party="Democrat", state="NY", chamber="House", in_office=True, photo_url="https://example.com/jane.jpg", url="https://www.smith.house.gov", ) mock_cache.save_legislator = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api}): resp = client.get("/api/legislators/S001") data = resp.get_json() assert resp.status_code == 200 assert data["full_name"] == "Jane Smith" assert data["chamber"] == "House" mock_cache.save_legislator.assert_called_once() def test_legislator_not_found(app, client): mock_cache = MagicMock() mock_api = MagicMock() mock_cache.get_legislator.return_value = None mock_api.get_legislator.return_value = None with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api}): resp = client.get("/api/legislators/MISSING") assert resp.status_code == 404 assert "not found" in resp.get_json()["error"].lower() def test_legislator_api_error(app, client): mock_cache = MagicMock() mock_api = MagicMock() mock_cache.get_legislator.return_value = None mock_api.get_legislator.side_effect = Exception("Network error") with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api}): resp = client.get("/api/legislators/M000355") assert resp.status_code == 503 # --- Legislator votes endpoint --- def test_legislator_votes_cached(app, client): mock_cache = MagicMock() mock_vote_client = MagicMock() mock_leg = Legislator( id="L001", first_name="John", last_name="Doe", full_name="John Doe", party="Democrat", state="CA", chamber="Senate", in_office=True, end_date=None, ) mock_cache.get_legislator.return_value = mock_leg mock_cache.get_voting_record.return_value = ( [ { "roll_call_id": "RC001", "vote_type": "Yea", "vote_date": "2025-06-01", "bill_id": "119/HR/1", "bill_title": "Test Bill", "subject": "Education", "sponsor": "John Doe", "enacted": False, "summary": "Summary text", "key_measures": ["Measure 1"], "generated_at": None, } ], 1, ) mock_cache.get_bill = MagicMock(return_value=MagicMock()) mock_cache.get_summary = MagicMock(return_value=MagicMock()) with patch.dict(app.config, {"CACHE": mock_cache, "VOTE_CLIENT": mock_vote_client}): resp = client.get("/api/legislators/L001/votes") data = resp.get_json() assert resp.status_code == 200 assert len(data["votes"]) == 1 assert data["votes"][0]["vote_type"] == "Yea" assert data["total"] == 1 mock_vote_client.get_legislator_votes.assert_not_called() def test_legislator_votes_pagination(app, client): mock_cache = MagicMock() mock_vote_client = MagicMock() mock_leg = Legislator( id="L001", first_name="John", last_name="Doe", full_name="John Doe", party="Democrat", state="CA", chamber="Senate", in_office=True, end_date=None, ) mock_cache.get_legislator.return_value = mock_leg mock_cache.get_voting_record.side_effect = [ ([], 0), ( [ { "roll_call_id": "RC001", "vote_type": "Yea", "vote_date": "2025-06-01", "bill_id": "", "bill_title": "Bill A", "subject": "", "sponsor": "", "enacted": False, "summary": "", "key_measures": [], "generated_at": None, } ], 1, ), ] mock_vote_client.get_legislator_votes.return_value = [ Vote(legislator_id="L001", roll_call_id="RC001", vote_type="Yea", bill_title="Bill A"), ] mock_cache.save_votes = MagicMock() mock_cache.get_bill = MagicMock(return_value=MagicMock()) mock_cache.get_summary = MagicMock(return_value=MagicMock()) with patch.dict(app.config, {"CACHE": mock_cache, "VOTE_CLIENT": mock_vote_client}): resp = client.get("/api/legislators/L001/votes?limit=5&offset=0") data = resp.get_json() assert resp.status_code == 200 assert data["limit"] == 5 assert data["offset"] == 0 def test_legislator_votes_retired_senator_congress(app, client): """Retired senator with end_date in Dec should query that congress.""" mock_cache = MagicMock() mock_vote_client = MagicMock() mock_leg = Legislator( id="M000355", first_name="Mitch", last_name="McConnell", full_name="Mitch McConnell", party="Republican", state="KY", chamber="Senate", in_office=False, start_date=date(1985, 1, 3), end_date=date(2025, 12, 31), ) mock_cache.get_legislator.return_value = mock_leg mock_cache.get_voting_record.return_value = ([], 0) mock_vote_client.get_legislator_votes.return_value = [] mock_cache.save_votes = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache, "VOTE_CLIENT": mock_vote_client}): resp = client.get("/api/legislators/M000355/votes") assert resp.status_code == 200 call_args = mock_vote_client.get_legislator_votes.call_args congress_arg = call_args.kwargs.get("congress", call_args[0][1] if call_args[0] else None) # end_date 2025-12-31: congress = 1 + (2025 - 1789) // 2 = 119, no Jan adjustment assert congress_arg == 119 def test_legislator_votes_retired_senator_jan_end_date(app, client): """Retired senator with end_date Jan 3 should query prior congress.""" mock_cache = MagicMock() mock_vote_client = MagicMock() mock_leg = Legislator( id="X000001", first_name="Former", last_name="Senator", full_name="Former Senator", party="Republican", state="KY", chamber="Senate", in_office=False, start_date=date(2023, 1, 3), end_date=date(2025, 1, 3), ) mock_cache.get_legislator.return_value = mock_leg mock_cache.get_voting_record.return_value = ([], 0) mock_vote_client.get_legislator_votes.return_value = [] mock_cache.save_votes = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache, "VOTE_CLIENT": mock_vote_client}): resp = client.get("/api/legislators/X000001/votes") assert resp.status_code == 200 call_args = mock_vote_client.get_legislator_votes.call_args congress_arg = call_args.kwargs.get("congress", call_args[0][1] if call_args[0] else None) # end_date 2025-01-03: congress = 1 + (2025 - 1789) // 2 = 119, Jan 3 adjustment -> 118 assert congress_arg == 118 def test_legislator_votes_congress_param(app, client): """Explicit congress param overrides automatic calculation.""" mock_cache = MagicMock() mock_vote_client = MagicMock() mock_leg = Legislator( id="L001", first_name="John", last_name="Doe", full_name="John Doe", party="Democrat", state="CA", chamber="Senate", in_office=True, start_date=date(2025, 1, 3), end_date=None, ) mock_cache.get_legislator.return_value = mock_leg mock_cache.get_voting_record.return_value = ([], 0) mock_vote_client.get_legislator_votes.return_value = [] mock_cache.save_votes = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache, "VOTE_CLIENT": mock_vote_client}): resp = client.get("/api/legislators/L001/votes?congress=117") assert resp.status_code == 200 call_args = mock_vote_client.get_legislator_votes.call_args congress_arg = call_args.kwargs.get("congress", call_args[0][1] if call_args[0] else None) assert congress_arg == 117 # --- Bill summary endpoint --- def test_bill_summary_cached(app, client): from src.models import Summary mock_cache = MagicMock() mock_cache.get_summary.return_value = Summary( bill_id="119/HR/1", summary_text="This bill does X.", key_measures=["Measure A"], model_name="gpt-oss", ) with patch.dict(app.config, {"CACHE": mock_cache}): resp = client.get("/api/bills/119/HR/1/summary") data = resp.get_json() assert resp.status_code == 200 assert data["summary_text"] == "This bill does X." assert data["key_measures"] == ["Measure A"] def test_bill_summary_not_found(app, client): mock_cache = MagicMock() mock_api = MagicMock() mock_cache.get_summary.return_value = None mock_cache.get_bill.return_value = None mock_api.get_bill_text.return_value = None with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api}): resp = client.get("/api/bills/999/HR/999/summary") assert resp.status_code == 404 # --- Bill text endpoint --- def test_bill_text_cached(app, client): from src.models import Bill mock_cache = MagicMock() mock_cache.get_bill.return_value = Bill( bill_id="119/HR/1", title="Test Bill", subject="Education", text="Full text here", sponsor="John Doe", enacted=True, ) with patch.dict(app.config, {"CACHE": mock_cache}): resp = client.get("/api/bills/119/HR/1/text") data = resp.get_json() assert resp.status_code == 200 assert data["title"] == "Test Bill" assert data["enacted"] is True def test_bill_text_not_found(app, client): mock_cache = MagicMock() mock_api = MagicMock() mock_cache.get_bill.return_value = None mock_api.get_bill_text.return_value = None with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api}): resp = client.get("/api/bills/999/HR/999/text") assert resp.status_code == 404 # --- Catch-all --- def test_catch_all_api(client): resp = client.get("/api/nonexistent") assert resp.status_code == 404 def test_root_redirect(client): resp = client.get("/") assert resp.status_code in (302, 301)