"""
Test suite for Congressional People & Relationships Hub bucket tool.
Tests operation-level access control and parameter routing across
member and committee operations.
"""
import pytest
from unittest.mock import Mock, AsyncMock, patch
from fastmcp import Context
from fastmcp.exceptions import ToolError
from congress_api.features.buckets.people_relationships_hub import (
congressional_people_relationships_hub,
check_operation_access,
route_people_relationships_operation,
FREE_OPERATIONS,
PAID_OPERATIONS
)
from congress_api.core.auth.auth import SubscriptionTier
class TestOperationAccessControl:
"""Test operation-level access control for different user tiers."""
@pytest.fixture
def mock_context_free(self):
"""Mock context for free tier user."""
ctx = Mock(spec=Context)
with patch('congress_api.features.buckets.people_relationships_hub.get_user_tier_from_context',
return_value=SubscriptionTier.FREE):
yield ctx
@pytest.fixture
def mock_context_pro(self):
"""Mock context for Pro tier user."""
ctx = Mock(spec=Context)
with patch('congress_api.features.buckets.people_relationships_hub.get_user_tier_from_context',
return_value=SubscriptionTier.PRO):
yield ctx
def test_free_operations_accessible_to_all_tiers(self, mock_context_free, mock_context_pro):
"""Test that free operations are accessible to all user tiers."""
for operation in FREE_OPERATIONS:
# Should not raise any exception for any tier
check_operation_access(mock_context_free, operation)
check_operation_access(mock_context_pro, operation)
def test_paid_operations_blocked_for_free_tier(self, mock_context_free):
"""Test that paid operations are blocked for free tier users."""
for operation in PAID_OPERATIONS:
with pytest.raises(ToolError, match="requires a paid subscription"):
check_operation_access(mock_context_free, operation)
def test_paid_operations_accessible_to_paid_tier(self, mock_context_pro):
"""Test that paid operations are accessible to Pro tier."""
for operation in PAID_OPERATIONS:
# Should not raise any exception for paid tier
check_operation_access(mock_context_pro, operation)
def test_unknown_operation_raises_error(self, mock_context_pro):
"""Test that unknown operations raise appropriate errors."""
with pytest.raises(ToolError, match="Unknown operation: invalid_operation"):
check_operation_access(mock_context_pro, "invalid_operation")
class TestParameterRouting:
"""Test parameter routing to underlying tool functions."""
@pytest.fixture
def mock_context(self):
"""Mock context for routing tests."""
return Mock(spec=Context)
@pytest.mark.asyncio
async def test_member_operations_routing(self, mock_context):
"""Test that member operations route to correct functions."""
with patch('congress_api.features.members.search_members', new_callable=AsyncMock) as mock_search:
mock_search.return_value = "mock member search results"
result = await route_people_relationships_operation(
mock_context,
"search_members",
name="Smith",
state="CA",
limit=5
)
mock_search.assert_called_once_with(
mock_context,
name="Smith",
state="CA",
limit=5
)
assert result == "mock member search results"
@pytest.mark.asyncio
async def test_committee_operations_routing(self, mock_context):
"""Test that committee operations route to correct functions."""
with patch('congress_api.features.committees.search_committees', new_callable=AsyncMock) as mock_search:
mock_search.return_value = "mock committee search results"
result = await route_people_relationships_operation(
mock_context,
"search_committees",
keywords="agriculture",
chamber="house"
)
mock_search.assert_called_once_with(
mock_context,
keywords="agriculture",
chamber="house"
)
assert result == "mock committee search results"
@pytest.mark.asyncio
async def test_advanced_member_operations_routing(self, mock_context):
"""Test that advanced member operations route correctly."""
with patch('congress_api.features.members.get_member_sponsored_legislation', new_callable=AsyncMock) as mock_legislation:
mock_legislation.return_value = "mock sponsored legislation"
result = await route_people_relationships_operation(
mock_context,
"get_member_sponsored_legislation",
bioguide_id="A000055"
)
mock_legislation.assert_called_once_with(
mock_context,
bioguide_id="A000055"
)
assert result == "mock sponsored legislation"
@pytest.mark.asyncio
async def test_unknown_operation_routing_error(self, mock_context):
"""Test that unknown operations raise routing errors."""
with pytest.raises(ToolError, match="Unknown operation: invalid_operation"):
await route_people_relationships_operation(mock_context, "invalid_operation")
class TestBucketToolIntegration:
"""Test the main bucket tool function with full integration."""
@pytest.fixture
def mock_context_free(self):
"""Mock context for free tier user."""
ctx = Mock(spec=Context)
with patch('congress_api.features.buckets.people_relationships_hub.get_user_tier_from_context',
return_value=SubscriptionTier.FREE):
yield ctx
@pytest.fixture
def mock_context_pro(self):
"""Mock context for Pro tier user."""
ctx = Mock(spec=Context)
with patch('congress_api.features.buckets.people_relationships_hub.get_user_tier_from_context',
return_value=SubscriptionTier.PRO):
yield ctx
@pytest.mark.asyncio
async def test_free_user_can_access_free_operations(self, mock_context_free):
"""Test that free users can access free operations."""
with patch('congress_api.features.members.search_members', new_callable=AsyncMock) as mock_search:
mock_search.return_value = "mock member search results"
result = await congressional_people_relationships_hub(
mock_context_free,
operation="search_members",
name="Smith",
state="CA"
)
assert result == "mock member search results"
mock_search.assert_called_once()
@pytest.mark.asyncio
async def test_free_user_blocked_from_paid_operations(self, mock_context_free):
"""Test that free users are blocked from paid operations."""
with pytest.raises(ToolError, match="requires a paid subscription"):
await congressional_people_relationships_hub(
mock_context_free,
operation="get_member_sponsored_legislation", # This is a paid operation
bioguide_id="A000055"
)
@pytest.mark.asyncio
async def test_paid_user_can_access_all_operations(self, mock_context_pro):
"""Test that paid users can access both free and paid operations."""
# Test free operation
with patch('congress_api.features.members.search_members', new_callable=AsyncMock) as mock_search:
mock_search.return_value = "mock member search results"
result = await congressional_people_relationships_hub(
mock_context_pro,
operation="search_members",
name="Smith"
)
assert result == "mock member search results"
# Test paid operation
with patch('congress_api.features.members.get_member_sponsored_legislation', new_callable=AsyncMock) as mock_legislation:
mock_legislation.return_value = "mock sponsored legislation"
result = await congressional_people_relationships_hub(
mock_context_pro,
operation="get_member_sponsored_legislation",
bioguide_id="A000055"
)
assert result == "mock sponsored legislation"
@pytest.mark.asyncio
async def test_parameter_filtering_and_passing(self, mock_context_pro):
"""Test that parameters are correctly filtered and passed to operations."""
with patch('congress_api.features.members.get_member_details', new_callable=AsyncMock) as mock_details:
mock_details.return_value = "mock member details"
# Call with both relevant and irrelevant parameters
result = await congressional_people_relationships_hub(
mock_context_pro,
operation="get_member_details",
bioguide_id="A000055",
state="CA",
party="D",
# These should be filtered out (not relevant to member details)
keywords=None, # Should be filtered out (None value)
committee_code="test"
)
# Verify parameters were passed correctly
mock_details.assert_called_once_with(
mock_context_pro,
bioguide_id="A000055",
state="CA",
party="D",
committee_code="test" # Currently all params passed, might refine later
)
assert result == "mock member details"
@pytest.mark.asyncio
async def test_committee_operations_integration(self, mock_context_pro):
"""Test committee operations work through the bucket."""
with patch('congress_api.features.committees.get_committee_bills', new_callable=AsyncMock) as mock_bills:
mock_bills.return_value = "mock committee bills"
result = await congressional_people_relationships_hub(
mock_context_pro,
operation="get_committee_bills",
chamber="house",
committee_code="hsag",
limit=10
)
mock_bills.assert_called_once_with(
mock_context_pro,
chamber="house",
committee_code="hsag",
limit=10
)
assert result == "mock committee bills"
if __name__ == "__main__":
pytest.main([__file__, "-v"])