| """Unit tests for MCP tool wrappers.""" |
|
|
| from unittest.mock import AsyncMock, patch |
|
|
| import pytest |
|
|
| from src.mcp_tools import ( |
| search_all_sources, |
| search_clinical_trials, |
| search_europepmc, |
| search_pubmed, |
| ) |
| from src.utils.models import Citation, Evidence |
|
|
|
|
| @pytest.fixture |
| def mock_evidence() -> Evidence: |
| """Sample evidence for testing.""" |
| return Evidence( |
| content="Metformin shows neuroprotective effects in preclinical models.", |
| citation=Citation( |
| source="pubmed", |
| title="Metformin and Alzheimer's Disease", |
| url="https://pubmed.ncbi.nlm.nih.gov/12345678/", |
| date="2024-01-15", |
| authors=["Smith J", "Jones M", "Brown K"], |
| ), |
| relevance=0.85, |
| ) |
|
|
|
|
| class TestSearchPubMed: |
| """Tests for search_pubmed MCP tool.""" |
|
|
| @pytest.mark.asyncio |
| async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None: |
| """Should return formatted markdown string.""" |
| with patch("src.mcp_tools._pubmed") as mock_tool: |
| mock_tool.search = AsyncMock(return_value=[mock_evidence]) |
|
|
| result = await search_pubmed("metformin alzheimer", 10) |
|
|
| assert isinstance(result, str) |
| assert "PubMed Results" in result |
| assert "Metformin and Alzheimer's Disease" in result |
| assert "Smith J" in result |
|
|
| @pytest.mark.asyncio |
| async def test_clamps_max_results(self) -> None: |
| """Should clamp max_results to valid range (1-50).""" |
| with patch("src.mcp_tools._pubmed") as mock_tool: |
| mock_tool.search = AsyncMock(return_value=[]) |
|
|
| |
| await search_pubmed("test", 0) |
| mock_tool.search.assert_called_with("test", 1) |
|
|
| |
| await search_pubmed("test", 100) |
| mock_tool.search.assert_called_with("test", 50) |
|
|
| @pytest.mark.asyncio |
| async def test_handles_no_results(self) -> None: |
| """Should return appropriate message when no results.""" |
| with patch("src.mcp_tools._pubmed") as mock_tool: |
| mock_tool.search = AsyncMock(return_value=[]) |
|
|
| result = await search_pubmed("xyznonexistent", 10) |
|
|
| assert "No PubMed results found" in result |
|
|
|
|
| class TestSearchClinicalTrials: |
| """Tests for search_clinical_trials MCP tool.""" |
|
|
| @pytest.mark.asyncio |
| async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None: |
| """Should return formatted markdown string.""" |
| mock_evidence.citation.source = "clinicaltrials" |
|
|
| with patch("src.mcp_tools._trials") as mock_tool: |
| mock_tool.search = AsyncMock(return_value=[mock_evidence]) |
|
|
| result = await search_clinical_trials("diabetes", 10) |
|
|
| assert isinstance(result, str) |
| assert "Clinical Trials" in result |
|
|
|
|
| class TestSearchEuropePMC: |
| """Tests for search_europepmc MCP tool.""" |
|
|
| @pytest.mark.asyncio |
| async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None: |
| """Should return formatted markdown string.""" |
| mock_evidence.citation.source = "europepmc" |
|
|
| with patch("src.mcp_tools._europepmc") as mock_tool: |
| mock_tool.search = AsyncMock(return_value=[mock_evidence]) |
|
|
| result = await search_europepmc("preprint search", 10) |
|
|
| assert isinstance(result, str) |
| assert "Europe PMC Results" in result |
|
|
|
|
| class TestSearchAllSources: |
| """Tests for search_all_sources MCP tool.""" |
|
|
| @pytest.mark.asyncio |
| async def test_combines_all_sources(self, mock_evidence: Evidence) -> None: |
| """Should combine results from all sources.""" |
| with ( |
| patch("src.mcp_tools.search_pubmed", new_callable=AsyncMock) as mock_pubmed, |
| patch("src.mcp_tools.search_clinical_trials", new_callable=AsyncMock) as mock_trials, |
| patch("src.mcp_tools.search_europepmc", new_callable=AsyncMock) as mock_europepmc, |
| ): |
| mock_pubmed.return_value = "## PubMed Results" |
| mock_trials.return_value = "## Clinical Trials" |
| mock_europepmc.return_value = "## Europe PMC Results" |
|
|
| result = await search_all_sources("metformin", 5) |
|
|
| assert "Comprehensive Search" in result |
| assert "PubMed" in result |
| assert "Clinical Trials" in result |
| assert "Europe PMC" in result |
|
|
| @pytest.mark.asyncio |
| async def test_handles_partial_failures(self) -> None: |
| """Should handle partial failures gracefully.""" |
| with ( |
| patch("src.mcp_tools.search_pubmed", new_callable=AsyncMock) as mock_pubmed, |
| patch("src.mcp_tools.search_clinical_trials", new_callable=AsyncMock) as mock_trials, |
| patch("src.mcp_tools.search_europepmc", new_callable=AsyncMock) as mock_europepmc, |
| ): |
| mock_pubmed.return_value = "## PubMed Results" |
| mock_trials.side_effect = Exception("API Error") |
| mock_europepmc.return_value = "## Europe PMC Results" |
|
|
| result = await search_all_sources("metformin", 5) |
|
|
| |
| assert "PubMed" in result |
| assert "Europe PMC" in result |
| |
| assert "Error" in result |
|
|
|
|
| class TestMCPDocstrings: |
| """Tests that docstrings follow MCP format.""" |
|
|
| def test_search_pubmed_has_args_section(self) -> None: |
| """Docstring must have Args section for MCP schema generation.""" |
| assert search_pubmed.__doc__ is not None |
| assert "Args:" in search_pubmed.__doc__ |
| assert "query:" in search_pubmed.__doc__ |
| assert "max_results:" in search_pubmed.__doc__ |
| assert "Returns:" in search_pubmed.__doc__ |
|
|
| def test_search_clinical_trials_has_args_section(self) -> None: |
| """Docstring must have Args section for MCP schema generation.""" |
| assert search_clinical_trials.__doc__ is not None |
| assert "Args:" in search_clinical_trials.__doc__ |
|
|
| def test_search_europepmc_has_args_section(self) -> None: |
| """Docstring must have Args section for MCP schema generation.""" |
| assert search_europepmc.__doc__ is not None |
| assert "Args:" in search_europepmc.__doc__ |
|
|
| def test_search_all_sources_has_args_section(self) -> None: |
| """Docstring must have Args section for MCP schema generation.""" |
| assert search_all_sources.__doc__ is not None |
| assert "Args:" in search_all_sources.__doc__ |
|
|
|
|
| class TestMCPTypeHints: |
| """Tests that type hints are complete for MCP.""" |
|
|
| def test_search_pubmed_type_hints(self) -> None: |
| """All parameters and return must have type hints.""" |
| import inspect |
|
|
| sig = inspect.signature(search_pubmed) |
|
|
| |
| assert sig.parameters["query"].annotation is str |
| assert sig.parameters["max_results"].annotation is int |
|
|
| |
| assert sig.return_annotation is str |
|
|
| def test_search_clinical_trials_type_hints(self) -> None: |
| """All parameters and return must have type hints.""" |
| import inspect |
|
|
| sig = inspect.signature(search_clinical_trials) |
| assert sig.parameters["query"].annotation is str |
| assert sig.parameters["max_results"].annotation is int |
| assert sig.return_annotation is str |
|
|