Skip to main content
Glama
test_server.py16.9 kB
""" Tests for the ProPublica MCP server. This module contains unit tests for the MCP server tools, including mocked API responses and tool validation testing. """ import pytest import asyncio import json from unittest.mock import AsyncMock, Mock, patch # Import the server tools directly from src.propublica_mcp.server import ( search_nonprofits, get_organization, get_organization_filings, analyze_nonprofit_financials, search_similar_nonprofits, export_nonprofit_data ) from src.propublica_mcp.models import ( NonprofitOrganization, Filing, SearchResult, FinancialSummary ) from src.propublica_mcp.api_client import ProPublicaAPIError @pytest.fixture def mock_search_result(): """Mock search result for testing.""" org1 = NonprofitOrganization( ein="123456789", name="Test Nonprofit 1", state="CA", ntee_code="A01", city="Test City", address="123 Main St", zipcode="12345", subseccd="3" ) return SearchResult( total_results=1, num_pages=1, cur_page=0, per_page=25, page_offset=0, organizations=[org1] ) @pytest.fixture def mock_organization(): """Mock organization for testing.""" return NonprofitOrganization( ein="123456789", name="Test Nonprofit", address="123 Main St", city="Test City", state="CA", zipcode="12345", subseccd="3", ntee_code="A01" ) @pytest.fixture def mock_filings(): """Mock filings for testing.""" return [ Filing( ein="123456789", tax_year=2022, form_type="990", pdf_url="https://example.com/filing1.pdf", totrevenue=1000000.0, totfuncexpns=800000.0, totassetsend=600000.0, totliabend=100000.0 ), Filing( ein="123456789", tax_year=2021, form_type="990", pdf_url="https://example.com/filing2.pdf", totrevenue=950000.0, totfuncexpns=750000.0, totassetsend=550000.0, totliabend=100000.0 ) ] class TestServerTools: """Test suite for MCP server tools.""" @pytest.mark.asyncio async def test_search_nonprofits_basic(self, mock_search_result): """Test basic nonprofit search functionality.""" with patch('src.propublica_mcp.server.api_client.search_organizations', new_callable=AsyncMock) as mock_search: mock_search.return_value = mock_search_result result = await search_nonprofits(query="education") result_data = json.loads(result) assert "organizations" in result_data assert "pagination" in result_data assert "search_metadata" in result_data assert len(result_data["organizations"]) == 1 assert result_data["organizations"][0]["ein"] == "123456789" assert result_data["search_query"] == "education" assert "generated_at" in result_data mock_search.assert_called_once_with( query="education", state=None, ntee_category=None, subsection_code=None, page=0, limit=25 ) @pytest.mark.asyncio async def test_search_nonprofits_with_filters(self, mock_search_result): """Test nonprofit search with filters.""" with patch('src.propublica_mcp.server.api_client.search_organizations', new_callable=AsyncMock) as mock_search: mock_search.return_value = mock_search_result result = await search_nonprofits( query="health", state="CA", ntee_code="4", subsection_code="3", page=1, per_page=10 ) result_data = json.loads(result) assert result_data["filters"]["state"] == "CA" assert result_data["filters"]["ntee_code"] == "4" assert result_data["filters"]["subsection_code"] == "3" assert result_data["pagination"]["page"] == 1 assert result_data["pagination"]["per_page"] == 10 mock_search.assert_called_once_with( query="health", state="CA", ntee_category=4, subsection_code=3, page=1, limit=10 ) @pytest.mark.asyncio async def test_search_nonprofits_invalid_state(self): """Test search with invalid state code.""" result = await search_nonprofits(query="test", state="XX") result_data = json.loads(result) assert "error" in result_data assert "Invalid state code" in result_data["error"] @pytest.mark.asyncio async def test_get_organization_success(self, mock_organization): """Test successful organization retrieval.""" with patch('src.propublica_mcp.server.api_client.get_organization', new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_organization result = await get_organization(ein="123456789") result_data = json.loads(result) assert "organization" in result_data assert result_data["organization"]["ein"] == "123456789" assert result_data["organization"]["name"] == "Test Nonprofit" assert "retrieved_at" in result_data mock_get.assert_called_once_with("123456789") @pytest.mark.asyncio async def test_get_organization_invalid_ein(self): """Test organization retrieval with invalid EIN.""" result = await get_organization(ein="invalid") result_data = json.loads(result) assert "error" in result_data assert "Invalid EIN format" in result_data["error"] @pytest.mark.asyncio async def test_get_organization_ein_with_hyphen(self, mock_organization): """Test organization retrieval with hyphenated EIN.""" with patch('src.propublica_mcp.server.api_client.get_organization', new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_organization result = await get_organization(ein="12-3456789") result_data = json.loads(result) assert "organization" in result_data # Should clean the EIN and call with clean version mock_get.assert_called_once_with("123456789") @pytest.mark.asyncio async def test_get_organization_filings_success(self, mock_filings): """Test successful filing retrieval.""" with patch('src.propublica_mcp.server.api_client.get_organization_filings', new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_filings result = await get_organization_filings(ein="123456789") result_data = json.loads(result) assert "filings" in result_data assert "filing_summary" in result_data assert len(result_data["filings"]) == 2 assert result_data["filings"][0]["tax_year"] == 2022 @pytest.mark.asyncio async def test_get_organization_filings_limit(self, mock_filings): """Test filing retrieval with limit.""" # Create more filings to test limiting extended_filings = mock_filings + [ Filing( ein="123456789", tax_year=2020, form_type="990", pdf_url="https://example.com/filing3.pdf", totrevenue=900000.0, totfuncexpns=700000.0, totassetsend=500000.0, totliabend=90000.0 ) ] with patch('src.propublica_mcp.server.api_client.get_organization_filings', new_callable=AsyncMock) as mock_get: mock_get.return_value = extended_filings result = await get_organization_filings(ein="123456789", limit=2) result_data = json.loads(result) assert len(result_data["filings"]) == 2 assert result_data["total_filings_available"] == 3 assert result_data["filings_returned"] == 2 @pytest.mark.asyncio async def test_analyze_nonprofit_financials_success(self, mock_organization, mock_filings): """Test financial analysis tool.""" with patch('src.propublica_mcp.server.api_client.get_organization', new_callable=AsyncMock) as mock_get_org: with patch('src.propublica_mcp.server.api_client.get_organization_filings', new_callable=AsyncMock) as mock_get_filings: mock_get_org.return_value = mock_organization mock_get_filings.return_value = mock_filings result = await analyze_nonprofit_financials(ein="123456789", years=2) result_data = json.loads(result) assert "financial_summary" in result_data assert "detailed_data" in result_data assert "trends" in result_data assert "ratios" in result_data summary = result_data["financial_summary"] assert summary["ein"] == "123456789" assert summary["organization_name"] == "Test Nonprofit" assert "filings_analyzed" in summary @pytest.mark.asyncio async def test_search_similar_nonprofits_success(self, mock_organization, mock_search_result): """Test similar nonprofits search.""" with patch('src.propublica_mcp.server.api_client.get_organization', new_callable=AsyncMock) as mock_get_org: with patch('src.propublica_mcp.server.api_client.search_organizations', new_callable=AsyncMock) as mock_search: mock_get_org.return_value = mock_organization mock_search.return_value = mock_search_result result = await search_similar_nonprofits( ein="123456789", same_ntee=True, min_revenue=500000, max_revenue=2000000, limit=5 ) result_data = json.loads(result) assert "similar_organizations" in result_data assert "reference_organization" in result_data assert "search_criteria" in result_data assert result_data["reference_organization"]["ein"] == "123456789" @pytest.mark.asyncio async def test_export_nonprofit_data_json(self, mock_organization, mock_filings): """Test nonprofit data export in JSON format.""" with patch('src.propublica_mcp.server.api_client.get_organization', new_callable=AsyncMock) as mock_get_org: with patch('src.propublica_mcp.server.api_client.get_organization_filings', new_callable=AsyncMock) as mock_get_filings: mock_get_org.return_value = mock_organization mock_get_filings.return_value = mock_filings result = await export_nonprofit_data( eins=["123456789"], format="json", include_financials=True, include_filings=True, max_filings_per_org=2 ) result_data = json.loads(result) assert "organizations" in result_data assert "metadata" in result_data assert len(result_data["organizations"]) == 1 org_data = result_data["organizations"][0] assert org_data["ein"] == "123456789" assert "organization_name" in org_data assert "recent_filings" in org_data @pytest.mark.asyncio async def test_export_nonprofit_data_csv(self, mock_organization): """Test nonprofit data export in CSV format.""" with patch('src.propublica_mcp.server.api_client.get_organization', new_callable=AsyncMock) as mock_get_org: mock_get_org.return_value = mock_organization result = await export_nonprofit_data( eins=["123456789"], format="csv", include_financials=False, include_filings=False ) result_data = json.loads(result) assert "csv_data" in result_data assert "export_metadata" in result_data # CSV should contain headers and data csv_content = result_data["csv_data"] assert "ein,organization_name" in csv_content assert "123456789,Test Nonprofit" in csv_content @pytest.mark.asyncio async def test_export_nonprofit_data_invalid_format(self): """Test export with invalid format.""" result = await export_nonprofit_data( eins=["123456789"], format="invalid" ) result_data = json.loads(result) assert "error" in result_data assert "Invalid format" in result_data["error"] @pytest.mark.asyncio async def test_error_handling_api_failure(self): """Test error handling when API calls fail.""" with patch('src.propublica_mcp.server.api_client.search_organizations', new_callable=AsyncMock) as mock_search: mock_search.side_effect = Exception("API connection failed") result = await search_nonprofits(query="test") result_data = json.loads(result) assert "error" in result_data assert "Search failed" in result_data["error"] assert "error_type" in result_data @pytest.mark.asyncio async def test_pagination_limits(self, mock_search_result): """Test pagination and limit handling.""" with patch('src.propublica_mcp.server.api_client.search_organizations', new_callable=AsyncMock) as mock_search: mock_search.return_value = mock_search_result # Test per_page limit (should cap at 25) result = await search_nonprofits(query="test", per_page=100) result_data = json.loads(result) assert result_data["pagination"]["per_page"] == 25 # Verify the API was called with the capped value call_args = mock_search.call_args assert call_args[1]["limit"] == 25 @pytest.mark.asyncio async def test_filing_limit_validation(self, mock_filings): """Test filing retrieval limit validation.""" with patch('src.propublica_mcp.server.api_client.get_organization_filings', new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_filings # Test limit over 100 (should cap at 100) result = await get_organization_filings(ein="123456789", limit=150) result_data = json.loads(result) # The limit should be applied in the function assert "filings" in result_data @pytest.mark.asyncio async def test_ein_cleaning_and_validation(self, mock_organization): """Test EIN cleaning and validation across tools.""" with patch('src.propublica_mcp.server.api_client.get_organization', new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_organization # Test various EIN formats test_eins = [ "123456789", # Clean "12-3456789", # With hyphen " 123456789 ", # With spaces ] for ein in test_eins: result = await get_organization(ein=ein) result_data = json.loads(result) assert "organization" in result_data # Should always call with clean EIN mock_get.assert_called_with("123456789") # Test invalid EINs invalid_eins = [ "12345678", # Too short "1234567890", # Too long "abcdefghi", # Not numeric "", # Empty ] for ein in invalid_eins: result = await get_organization(ein=ein) result_data = json.loads(result) assert "error" in result_data assert "Invalid EIN format" in result_data["error"] if __name__ == "__main__": pytest.main([__file__])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/asachs01/propublica-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server