voting-app/tests/e2e/test_e2e.py
2026-05-15 15:16:18 -05:00

366 lines
12 KiB
Python

"""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)