Skip to main content
Glama
chrismannina

PubMed MCP Server

by chrismannina
test_pubmed_client_extended.py14.2 kB
""" Extended test cases for PubMed client functionality. """ from datetime import datetime from unittest.mock import Mock, patch import httpx import pytest from src.models import ArticleType, DateRange, SortOrder from src.pubmed_client import PubMedClient class TestPubMedClientExtended: """Extended tests for PubMed client to improve coverage.""" @pytest.fixture def client(self): """Create a PubMed client for testing.""" return PubMedClient( api_key="test_api_key", email="test@example.com", rate_limit=10.0, # High rate limit for tests ) @pytest.fixture def mock_response_xml(self): """Create a mock XML response for testing.""" return """<?xml version="1.0" ?> <eSearchResult> <Count>1</Count> <RetMax>1</RetMax> <RetStart>0</RetStart> <QueryKey>1</QueryKey> <WebEnv>test_webenv</WebEnv> <IdList> <Id>12345678</Id> </IdList> </eSearchResult>""" @pytest.fixture def mock_detailed_xml(self): """Create a mock detailed XML response for testing.""" return """<?xml version="1.0" ?> <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>12345678</PMID> <Article PubModel="Electronic"> <Journal> <ISSN IssnType="Electronic">1234-5678</ISSN> <JournalIssue CitedMedium="Internet"> <Volume>25</Volume> <Issue>3</Issue> <PubDate> <Year>2023</Year> <Month>03</Month> <Day>15</Day> </PubDate> </JournalIssue> <Title>Journal of Cancer Research</Title> <ISOAbbreviation>J Cancer Res</ISOAbbreviation> </Journal> <ArticleTitle>A Sample Research Article on Cancer Treatment</ArticleTitle> <Abstract> <AbstractText>This is a sample abstract describing cancer treatment research.</AbstractText> </Abstract> <AuthorList CompleteYN="Y"> <Author ValidYN="Y"> <LastName>Smith</LastName> <ForeName>John</ForeName> <Initials>J</Initials> <AffiliationInfo> <Affiliation>University Hospital</Affiliation> </AffiliationInfo> </Author> <Author ValidYN="Y"> <LastName>Doe</LastName> <ForeName>Jane</ForeName> <Initials>J</Initials> </Author> </AuthorList> <Language>eng</Language> <PublicationTypeList> <PublicationType UI="D016428">Journal Article</PublicationType> </PublicationTypeList> </Article> </MedlineCitation> <PubmedData> <History> <PubMedPubDate PubStatus="received"> <Year>2023</Year> <Month>01</Month> <Day>01</Day> </PubMedPubDate> </History> <ArticleIdList> <ArticleId IdType="pubmed">12345678</ArticleId> <ArticleId IdType="doi">10.1000/example.doi</ArticleId> <ArticleId IdType="pmc">PMC1234567</ArticleId> </ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet>""" def create_mock_search_response(self, pmids=None, count="1"): """Helper to create mock search response.""" if pmids is None: pmids = ["12345678"] mock_response = Mock() mock_response.json.return_value = {"esearchresult": {"idlist": pmids, "count": count}} return mock_response def create_mock_link_response(self, pmids=None): """Helper to create mock link response.""" if pmids is None: pmids = ["12345678"] mock_response = Mock() mock_response.json.return_value = { "linksets": [{"linksetdbs": [{"linkname": "pubmed_pubmed", "links": pmids}]}] } return mock_response @pytest.mark.asyncio async def test_search_with_date_range_enum(self, client, mock_response_xml): """Test search with DateRange enum values.""" # Create a proper mock response object mock_response = Mock() mock_response.json.return_value = {"esearchresult": {"idlist": ["12345678"], "count": "1"}} with patch.object(client, "_make_request", return_value=mock_response): with patch.object(client, "_fetch_article_details", return_value=[]): result = await client.search_articles( query="cancer", date_range=DateRange.LAST_5_YEARS, max_results=10, ) assert result.total_results == 1 assert result.returned_results == 0 # No articles since we mock empty list @pytest.mark.asyncio async def test_search_with_custom_date_range(self, client, mock_response_xml): """Test search with custom date range.""" mock_response = self.create_mock_search_response() with patch.object(client, "_make_request", return_value=mock_response): with patch.object(client, "_fetch_article_details", return_value=[]): result = await client.search_articles( query="cancer", date_from="2020/01/01", date_to="2023/12/31", max_results=10, ) assert result.total_results == 1 @pytest.mark.asyncio async def test_search_with_article_types(self, client, mock_response_xml): """Test search with specific article types.""" mock_response = self.create_mock_search_response() with patch.object(client, "_make_request", return_value=mock_response): with patch.object(client, "_fetch_article_details", return_value=[]): result = await client.search_articles( query="cancer", article_types=[ArticleType.REVIEW, ArticleType.META_ANALYSIS], max_results=10, ) assert result.total_results == 1 @pytest.mark.asyncio async def test_search_with_sort_order(self, client, mock_response_xml): """Test search with different sort orders.""" mock_response = self.create_mock_search_response() with patch.object(client, "_make_request", return_value=mock_response): with patch.object(client, "_fetch_article_details", return_value=[]): result = await client.search_articles( query="cancer", sort_order=SortOrder.PUBLICATION_DATE, max_results=10, ) assert result.total_results == 1 @pytest.mark.asyncio async def test_search_with_all_parameters(self, client, mock_response_xml): """Test search with all parameters specified.""" mock_response = self.create_mock_search_response() with patch.object(client, "_make_request", return_value=mock_response): with patch.object(client, "_fetch_article_details", return_value=[]): result = await client.search_articles( query="cancer treatment", max_results=50, sort_order=SortOrder.RELEVANCE, date_range=DateRange.LAST_YEAR, article_types=[ArticleType.JOURNAL_ARTICLE], authors=["Smith J"], journals=["Nature"], mesh_terms=["Neoplasms"], humans_only=True, has_abstract=True, has_full_text=False, language="eng", ) assert result.total_results == 1 @pytest.mark.asyncio async def test_get_article_details_comprehensive(self, client, mock_detailed_xml): """Test getting comprehensive article details.""" # Create a proper mock response object with .text property mock_response = Mock() mock_response.text = mock_detailed_xml with patch.object(client, "_make_request", return_value=mock_response): articles = await client.get_article_details( pmids=["12345678"], include_abstracts=True, include_citations=True, ) assert len(articles) == 1 article = articles[0] assert article.pmid == "12345678" assert article.title == "A Sample Research Article on Cancer Treatment" assert len(article.authors) == 2 assert article.authors[0].last_name == "Smith" assert article.authors[0].affiliation == "University Hospital" @pytest.mark.asyncio async def test_search_by_author_comprehensive(self, client, mock_response_xml): """Test comprehensive author search.""" mock_response = self.create_mock_search_response() with patch.object(client, "_make_request", return_value=mock_response): with patch.object(client, "_fetch_article_details", return_value=[]): result = await client.search_by_author( author_name="Smith J", max_results=20, include_coauthors=True, ) assert result.total_results == 1 @pytest.mark.asyncio async def test_find_related_articles(self, client, mock_response_xml): """Test finding related articles.""" mock_response = self.create_mock_link_response() with patch.object(client, "_make_request", return_value=mock_response): with patch.object(client, "_fetch_article_details", return_value=[]): result = await client.find_related_articles( pmid="12345678", max_results=10, ) assert result.total_results == 1 @pytest.mark.asyncio async def test_make_request_with_retry(self, client): """Test make_request error handling.""" with patch("httpx.AsyncClient.get") as mock_get: # Create a mock response that will raise HTTPStatusError when raise_for_status is called mock_response = Mock() mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( "Rate limited", request=Mock(), response=Mock(status_code=429) ) mock_get.return_value = mock_response # Test that the error is properly raised with pytest.raises(httpx.HTTPStatusError, match="Rate limited"): await client._make_request("esearch.fcgi", {"param": "value"}) @pytest.mark.asyncio async def test_make_request_with_timeout(self, client): """Test make_request with timeout.""" with patch("httpx.AsyncClient.get") as mock_get: mock_get.side_effect = httpx.TimeoutException("Request timeout") with pytest.raises(httpx.TimeoutException): await client._make_request("esearch.fcgi", {"param": "value"}) @pytest.mark.asyncio async def test_make_request_with_connection_error(self, client): """Test make_request with connection error.""" with patch("httpx.AsyncClient.get") as mock_get: mock_get.side_effect = httpx.ConnectError("Connection failed") with pytest.raises(httpx.ConnectError): await client._make_request("esearch.fcgi", {"param": "value"}) @pytest.mark.asyncio async def test_close_client(self, client): """Test closing the HTTP client.""" with patch.object(client.client, "aclose") as mock_close: await client.close() mock_close.assert_called_once() @pytest.mark.asyncio async def test_close_client_with_error(self, client): """Test closing client when aclose raises an error.""" with patch.object(client.client, "aclose", side_effect=Exception("Close error")): # Should not raise exception - the close method should handle errors gracefully try: await client.close() except Exception: pytest.fail("close() should handle errors gracefully") @pytest.mark.asyncio async def test_rate_limiting_functionality(self, client): """Test that rate limiting is enforced.""" # Create a client with very low rate limit for testing slow_client = PubMedClient( api_key="test_key", email="test@example.com", rate_limit=10.0, # 10 requests per second - fast enough for testing ) with patch.object(slow_client, "_make_request", return_value=Mock(text="<test>")): start_time = datetime.now() # Make two requests await slow_client._make_request("esearch.fcgi", {}) await slow_client._make_request("esearch.fcgi", {}) elapsed = (datetime.now() - start_time).total_seconds() # Should take some time due to rate limiting, but not too much for testing assert elapsed >= 0 # Just ensure it doesn't fail

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/chrismannina/pubmed-mcp'

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