voting-app/tests/unit/test_app.py
2026-05-15 15:16:18 -05:00

425 lines
13 KiB
Python

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