"""End-to-end tests for the Voting App. Tests the full application flow through the Flask test client, with mocked external API calls to simulate real behavior. """ 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 Bill, Legislator, Summary, Vote @pytest.fixture def app(): a = create_app() a.config["TESTING"] = True return a @pytest.fixture def client(app): return app.test_client() def test_full_search_to_votes_flow(app, client): """Search for a legislator, view detail, get voting record.""" mock_api = MagicMock() mock_cache = MagicMock() mock_cache.search_legislators.return_value = [] mock_api.search_legislators.return_value = [ 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), photo_url="https://example.com/mcconnell.jpg", ) ] mock_cache.save_legislator = MagicMock() # Step 1: Search with patch.dict(app.config, {"API": mock_api, "CACHE": mock_cache}): resp = client.get("/api/search?q=mitch") data = resp.get_json() assert len(data) == 1 assert data[0]["id"] == "M000355" assert data[0]["full_name"] == "Mitch McConnell" # Step 2: Detail mock_cache2 = MagicMock() mock_api2 = MagicMock() mock_cache2.get_legislator.return_value = None mock_api2.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, start_date=date(1985, 1, 3), end_date=date(2025, 12, 31), photo_url="https://example.com/mcconnell.jpg", url="https://www.mcconnell.senate.gov", ) mock_cache2.save_legislator = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache2, "API": mock_api2}): resp = client.get("/api/legislators/M000355") data = resp.get_json() assert data["id"] == "M000355" assert data["in_office"] is False # Step 3: Votes mock_cache3 = MagicMock() mock_vote_client3 = MagicMock() mock_cache3.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, start_date=date(1985, 1, 3), end_date=date(2025, 12, 31), ) mock_cache3.get_voting_record.return_value = ( [ { "roll_call_id": "1", "vote_type": "Yea", "vote_date": "2025-07-01", "bill_id": "118/hr/1", "bill_title": "Education Bill", "subject": "Education", "sponsor": "John Doe", "enacted": True, "summary": "Bill summary", "key_measures": ["Funding increase"], "generated_at": None, } ], 1, ) mock_cache3.get_bill = MagicMock(return_value=MagicMock()) mock_cache3.get_summary = MagicMock(return_value=MagicMock()) mock_cache3.save_votes = MagicMock() mock_api3 = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache3, "API": mock_api3, "VOTE_CLIENT": mock_vote_client3}): resp = client.get("/api/legislators/M000355/votes") data = resp.get_json() assert data["legislator_id"] == "M000355" assert len(data["votes"]) == 1 assert data["votes"][0]["vote_type"] == "Yea" assert data["total"] == 1 def test_active_legislator_search_and_votes(app, client): """Search for an active senator, view their votes.""" mock_api = MagicMock() mock_cache = MagicMock() mock_cache.search_legislators.return_value = [] mock_api.search_legislators.return_value = [ Legislator( id="S001234", first_name="Jeff", last_name="Tester", full_name="Jeff Tester", party="Democrat", state="ND", chamber="Senate", in_office=True, start_date=date(2025, 1, 3), end_date=None, photo_url="https://example.com/tester.jpg", ) ] mock_cache.save_legislator = MagicMock() with patch.dict(app.config, {"API": mock_api, "CACHE": mock_cache}): resp = client.get("/api/search?q=tester") data = resp.get_json() assert len(data) == 1 assert data[0]["in_office"] is True mock_cache2 = MagicMock() mock_api2 = MagicMock() mock_api2.get_legislator.return_value = Legislator( id="S001234", first_name="Jeff", last_name="Tester", full_name="Jeff Tester", party="Democrat", state="ND", chamber="Senate", in_office=True, start_date=date(2025, 1, 3), end_date=None, ) mock_vote_client2 = MagicMock() mock_cache2.get_legislator.return_value = Legislator( id="S001234", first_name="Jeff", last_name="Tester", full_name="Jeff Tester", party="Democrat", state="ND", chamber="Senate", in_office=True, start_date=date(2025, 1, 3), end_date=None, ) mock_vote_client2.get_legislator_votes.return_value = [ Vote( legislator_id="S001234", roll_call_id="50", vote_type="Nay", bill_id="119/hr/100", bill_title="Defense Bill", ), Vote( legislator_id="S001234", roll_call_id="51", vote_type="Yea", bill_id="119/s/200", bill_title="Climate Bill", ), ] mock_cache2.get_voting_record.side_effect = [ ([], 0), ( [ { "roll_call_id": "50", "vote_type": "Nay", "vote_date": "2025-06-01", "bill_id": "119/hr/100", "bill_title": "Defense Bill", "subject": "", "sponsor": "", "enacted": False, "summary": "", "key_measures": [], "generated_at": None, }, { "roll_call_id": "51", "vote_type": "Yea", "vote_date": "2025-06-02", "bill_id": "119/s/200", "bill_title": "Climate Bill", "subject": "", "sponsor": "", "enacted": False, "summary": "", "key_measures": [], "generated_at": None, }, ], 2, ), ] mock_cache2.save_votes = MagicMock() mock_cache2.get_bill = MagicMock(return_value=MagicMock()) mock_cache2.get_summary = MagicMock(return_value=MagicMock()) with patch.dict(app.config, {"CACHE": mock_cache2, "API": mock_api2, "VOTE_CLIENT": mock_vote_client2}): resp = client.get("/api/legislators/S001234/votes") data = resp.get_json() assert len(data["votes"]) == 2 assert data["votes"][0]["vote_type"] == "Nay" assert data["votes"][1]["vote_type"] == "Yea" call_args = mock_vote_client2.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 == 119 def test_health_check(client): """Health endpoint returns ok status.""" resp = client.get("/health") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "ok" def test_bill_summary_flow(app, client): """Fetch bill text, generate summary, retrieve it.""" mock_cache = MagicMock() mock_api = MagicMock() mock_gpt = MagicMock() mock_cache.get_bill.return_value = None mock_cache.get_summary.return_value = None mock_api.get_bill_text.return_value = Bill( bill_id="119/hr/1", title="Education Act", subject="Education", text="Full bill text here", summary=None, sponsor="John Doe", ) mock_gpt.generate_summary.return_value = Summary( bill_id="119/hr/1", summary_text="This bill increases education funding.", key_measures=["Increases funding", "Expands access"], model_name="gpt-oss", ) mock_cache.save_bill = MagicMock() mock_cache.save_summary = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api, "GPT": mock_gpt}): resp = client.get("/api/bills/119/hr/1/summary") data = resp.get_json() assert resp.status_code == 200 assert data["summary_text"] == "This bill increases education funding." assert data["key_measures"] == ["Increases funding", "Expands access"] def test_bill_text_flow(app, client): """Fetch and retrieve bill text.""" mock_cache = MagicMock() mock_api = MagicMock() mock_cache.get_bill.return_value = None mock_api.get_bill_text.return_value = Bill( bill_id="119/hr/1", title="Education Act", subject="Education", text="Full bill text here", sponsor="John Doe", enacted=True, ) mock_cache.save_bill = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api}): resp = client.get("/api/bills/119/hr/1/text") data = resp.get_json() assert resp.status_code == 200 assert data["title"] == "Education Act" assert data["enacted"] is True def test_congress_param_overrides(app, client): """Explicit congress query param overrides automatic calculation.""" mock_cache = MagicMock() mock_vote_client = MagicMock() mock_cache.get_legislator.return_value = Legislator( id="L001", first_name="John", last_name="Doe", full_name="John Doe", party="Democrat", state="CA", chamber="House", in_office=True, start_date=date(2025, 1, 3), end_date=None, ) mock_cache.get_voting_record.return_value = ([], 0) mock_vote_client.get_legislator_votes.return_value = [] mock_cache.save_votes = MagicMock() mock_cache.get_bill = MagicMock(return_value=MagicMock()) mock_cache.get_summary = MagicMock(return_value=MagicMock()) mock_api = MagicMock() with patch.dict(app.config, {"CACHE": mock_cache, "API": mock_api, "VOTE_CLIENT": mock_vote_client}): resp = client.get("/api/legislators/L001/votes?congress=115") 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 == 115 def test_404_for_unknown_routes(app, client): """Unknown API routes return 404.""" resp = client.get("/api/unknown") assert resp.status_code == 404 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 def test_root_redirects(client): """Root path redirects to frontend.""" resp = client.get("/") assert resp.status_code in (301, 302)