"""
Test suite for MCP Wikipedia Server.
This module contains comprehensive tests for all Wikipedia tools and MCP protocol compliance.
"""
import pytest
import asyncio
import json
import time
from unittest.mock import Mock, patch, AsyncMock
from typing import Dict, Any, List
# Import the server module
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
try:
from mcp_server.mcp_server import WikipediaServer
except ImportError:
# Fallback for different import paths
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src', 'mcp_server'))
from mcp_server import WikipediaServer
class TestWikipediaServer:
"""Test cases for the WikipediaServer class."""
@pytest.fixture
def server(self):
"""Create a server instance for testing."""
return WikipediaServer()
@pytest.fixture
def mock_wikipedia_success(self):
"""Mock successful Wikipedia API responses."""
with patch('wikipedia.search') as mock_search, \
patch('wikipedia.page') as mock_page, \
patch('wikipedia.summary') as mock_summary:
# Mock search results
mock_search.return_value = ["Test Article", "Another Article"]
# Mock page object
mock_page_obj = Mock()
mock_page_obj.title = "Test Article"
mock_page_obj.url = "https://en.wikipedia.org/wiki/Test_Article"
mock_page_obj.content = "This is test content for the article."
mock_page_obj.sections = ["Introduction", "History", "Overview", "References"]
mock_page_obj.section = Mock(return_value="This is the history section content.")
mock_page_obj.pageid = 12345
mock_page_obj.categories = ["Test Category", "Example Category"]
mock_page_obj.links = ["Related Article 1", "Related Article 2"]
mock_page.return_value = mock_page_obj
# Mock summary
mock_summary.return_value = "This is a test summary of the article."
yield {
'search': mock_search,
'page': mock_page,
'summary': mock_summary,
'page_obj': mock_page_obj
}
class TestFetchWikipediaInfo:
"""Test cases for fetch_wikipedia_info tool."""
@pytest.fixture
def server(self):
return WikipediaServer()
@pytest.mark.asyncio
async def test_fetch_info_success(self, server):
"""Test successful Wikipedia article retrieval."""
with patch('wikipedia.search') as mock_search, \
patch('wikipedia.page') as mock_page:
# Setup mocks
mock_search.return_value = ["Python (programming language)"]
mock_page_obj = Mock()
mock_page_obj.title = "Python (programming language)"
mock_page_obj.url = "https://en.wikipedia.org/wiki/Python_(programming_language)"
mock_page_obj.summary = "Python is a high-level programming language."
mock_page_obj.pageid = 23862
mock_page_obj.categories = ["Programming languages", "Python"]
mock_page_obj.links = ["Programming", "Guido van Rossum"]
mock_page.return_value = mock_page_obj
# Test the function
result = await server.fetch_wikipedia_info("Python programming")
# Assertions
assert result["success"] is True
assert "data" in result
assert result["data"]["title"] == "Python (programming language)"
assert result["data"]["url"] == "https://en.wikipedia.org/wiki/Python_(programming_language)"
assert result["data"]["page_id"] == 23862
assert "summary" in result["data"]
assert "categories" in result["data"]
assert "links" in result["data"]
assert "metadata" in result
assert result["metadata"]["query"] == "Python programming"
@pytest.mark.asyncio
async def test_fetch_info_not_found(self, server):
"""Test handling of non-existent articles."""
with patch('wikipedia.search') as mock_search:
# Mock empty search results
mock_search.return_value = []
result = await server.fetch_wikipedia_info("NonExistentArticle12345")
assert result["success"] is False
assert "error" in result
assert "not found" in result["error"].lower()
assert result["metadata"]["query"] == "NonExistentArticle12345"
@pytest.mark.asyncio
async def test_fetch_info_disambiguation(self, server):
"""Test handling of disambiguation pages."""
import wikipedia
with patch('wikipedia.search') as mock_search, \
patch('wikipedia.page') as mock_page:
mock_search.return_value = ["Python", "Python (programming language)"]
# Mock disambiguation exception
mock_page.side_effect = wikipedia.DisambiguationError(
"Python",
["Python (programming language)", "Python (snake)", "Python (mythology)"]
)
result = await server.fetch_wikipedia_info("Python")
assert result["success"] is False
assert "disambiguation" in result["error"].lower() or "suggestions" in result
@pytest.mark.asyncio
async def test_fetch_info_network_error(self, server):
"""Test handling of network errors."""
with patch('wikipedia.search') as mock_search:
# Mock network error
mock_search.side_effect = Exception("Network connection failed")
result = await server.fetch_wikipedia_info("Test Query")
assert result["success"] is False
assert "error" in result
assert "metadata" in result
@pytest.mark.asyncio
async def test_fetch_info_empty_query(self, server):
"""Test handling of empty queries."""
result = await server.fetch_wikipedia_info("")
assert result["success"] is False
assert "error" in result
assert "empty" in result["error"].lower() or "invalid" in result["error"].lower()
@pytest.mark.asyncio
async def test_fetch_info_very_long_query(self, server):
"""Test handling of extremely long queries."""
long_query = "x" * 5000 # Very long query
result = await server.fetch_wikipedia_info(long_query)
# Should handle gracefully (either succeed or fail gracefully)
assert "success" in result
assert "metadata" in result
class TestListWikipediaSections:
"""Test cases for list_wikipedia_sections tool."""
@pytest.fixture
def server(self):
return WikipediaServer()
@pytest.mark.asyncio
async def test_list_sections_success(self, server):
"""Test successful section listing."""
with patch('wikipedia.page') as mock_page:
# Setup mock page with sections
mock_page_obj = Mock()
mock_page_obj.title = "Machine Learning"
mock_page_obj.sections = [
"Overview",
"History",
"Types",
"Supervised learning",
"Unsupervised learning",
"Applications",
"Ethics",
"References"
]
mock_page_obj.pageid = 233488
mock_page.return_value = mock_page_obj
result = await server.list_wikipedia_sections("Machine Learning")
assert result["success"] is True
assert "data" in result
assert result["data"]["article_title"] == "Machine Learning"
assert "sections" in result["data"]
assert len(result["data"]["sections"]) > 0
assert result["data"]["total_sections"] == len(result["data"]["sections"])
# Check section structure
first_section = result["data"]["sections"][0]
assert "title" in first_section
assert "level" in first_section
assert "index" in first_section
assert "metadata" in result
assert result["metadata"]["topic"] == "Machine Learning"
@pytest.mark.asyncio
async def test_list_sections_not_found(self, server):
"""Test handling when article is not found."""
import wikipedia
with patch('wikipedia.page') as mock_page:
# Mock page not found
mock_page.side_effect = wikipedia.PageError("Article not found")
result = await server.list_wikipedia_sections("NonExistentArticle")
assert result["success"] is False
assert "error" in result
assert "not found" in result["error"].lower()
@pytest.mark.asyncio
async def test_list_sections_no_sections(self, server):
"""Test handling of articles with no sections."""
with patch('wikipedia.page') as mock_page:
# Setup mock page with no sections
mock_page_obj = Mock()
mock_page_obj.title = "Simple Article"
mock_page_obj.sections = []
mock_page_obj.pageid = 12345
mock_page.return_value = mock_page_obj
result = await server.list_wikipedia_sections("Simple Article")
assert result["success"] is True
assert result["data"]["total_sections"] == 0
assert len(result["data"]["sections"]) == 0
class TestGetSectionContent:
"""Test cases for get_section_content tool."""
@pytest.fixture
def server(self):
return WikipediaServer()
@pytest.mark.asyncio
async def test_get_section_content_success(self, server):
"""Test successful section content retrieval."""
with patch('wikipedia.page') as mock_page:
# Setup mock page
mock_page_obj = Mock()
mock_page_obj.title = "Python (programming language)"
mock_page_obj.section.return_value = "Python was conceived in the late 1980s by Guido van Rossum..."
mock_page_obj.sections = ["Overview", "History", "Features", "References"]
mock_page_obj.pageid = 23862
mock_page.return_value = mock_page_obj
result = await server.get_section_content("Python programming", "History")
assert result["success"] is True
assert "data" in result
assert result["data"]["article_title"] == "Python (programming language)"
assert result["data"]["section_title"] == "History"
assert "content" in result["data"]
assert len(result["data"]["content"]) > 0
assert "Guido van Rossum" in result["data"]["content"]
assert "content_length" in result["data"]
assert result["data"]["content_length"] > 0
assert "metadata" in result
assert result["metadata"]["topic"] == "Python programming"
assert result["metadata"]["section_title"] == "History"
@pytest.mark.asyncio
async def test_get_section_content_section_not_found(self, server):
"""Test handling when section is not found."""
with patch('wikipedia.page') as mock_page:
# Setup mock page
mock_page_obj = Mock()
mock_page_obj.title = "Test Article"
mock_page_obj.section.return_value = None # Section not found
mock_page_obj.sections = ["Overview", "History", "References"]
mock_page_obj.pageid = 12345
mock_page.return_value = mock_page_obj
result = await server.get_section_content("Test Article", "NonExistentSection")
assert result["success"] is False
assert "error" in result
assert "section" in result["error"].lower()
assert "not found" in result["error"].lower()
@pytest.mark.asyncio
async def test_get_section_content_article_not_found(self, server):
"""Test handling when article is not found."""
import wikipedia
with patch('wikipedia.page') as mock_page:
# Mock article not found
mock_page.side_effect = wikipedia.PageError("Article not found")
result = await server.get_section_content("NonExistentArticle", "History")
assert result["success"] is False
assert "error" in result
assert "not found" in result["error"].lower()
@pytest.mark.asyncio
async def test_get_section_content_empty_section(self, server):
"""Test handling of empty section content."""
with patch('wikipedia.page') as mock_page:
# Setup mock page with empty section
mock_page_obj = Mock()
mock_page_obj.title = "Test Article"
mock_page_obj.section.return_value = "" # Empty section
mock_page_obj.sections = ["Overview", "EmptySection", "References"]
mock_page_obj.pageid = 12345
mock_page.return_value = mock_page_obj
result = await server.get_section_content("Test Article", "EmptySection")
assert result["success"] is True
assert result["data"]["content"] == ""
assert result["data"]["content_length"] == 0
class TestPerformance:
"""Performance and load testing."""
@pytest.fixture
def server(self):
return WikipediaServer()
@pytest.mark.asyncio
async def test_response_time_search(self, server):
"""Test that search response times are reasonable."""
with patch('wikipedia.search') as mock_search, \
patch('wikipedia.page') as mock_page:
# Setup fast mocks
mock_search.return_value = ["Test Article"]
mock_page_obj = Mock()
mock_page_obj.title = "Test Article"
mock_page_obj.summary = "Test summary"
mock_page_obj.url = "https://test.url"
mock_page_obj.pageid = 12345
mock_page_obj.categories = []
mock_page_obj.links = []
mock_page.return_value = mock_page_obj
# Measure response time
start_time = time.time()
result = await server.fetch_wikipedia_info("Test Query")
end_time = time.time()
response_time = end_time - start_time
assert result["success"] is True
assert response_time < 1.0 # Should complete within 1 second with mocks
@pytest.mark.asyncio
async def test_concurrent_requests(self, server):
"""Test handling of concurrent requests."""
with patch('wikipedia.search') as mock_search, \
patch('wikipedia.page') as mock_page:
# Setup mocks
mock_search.return_value = ["Test Article"]
mock_page_obj = Mock()
mock_page_obj.title = "Test Article"
mock_page_obj.summary = "Test summary"
mock_page_obj.url = "https://test.url"
mock_page_obj.pageid = 12345
mock_page_obj.categories = []
mock_page_obj.links = []
mock_page.return_value = mock_page_obj
# Create concurrent requests
tasks = [
server.fetch_wikipedia_info(f"Query {i}")
for i in range(5)
]
start_time = time.time()
results = await asyncio.gather(*tasks)
end_time = time.time()
total_time = end_time - start_time
# All requests should succeed
assert all(result["success"] for result in results)
# Should be faster than sequential execution
assert total_time < 5.0 # Much faster than 5 sequential requests
class TestErrorHandling:
"""Test error handling and edge cases."""
@pytest.fixture
def server(self):
return WikipediaServer()
@pytest.mark.asyncio
async def test_wikipedia_timeout(self, server):
"""Test handling of Wikipedia API timeouts."""
import wikipedia
with patch('wikipedia.search') as mock_search:
# Mock timeout exception
mock_search.side_effect = wikipedia.exceptions.HTTPTimeoutError("Timeout")
result = await server.fetch_wikipedia_info("Test Query")
assert result["success"] is False
assert "error" in result
assert "timeout" in result["error"].lower()
@pytest.mark.asyncio
async def test_rate_limit_handling(self, server):
"""Test handling of rate limiting."""
with patch('wikipedia.search') as mock_search:
# Mock rate limit exception
mock_search.side_effect = Exception("Rate limited")
result = await server.fetch_wikipedia_info("Test Query")
assert result["success"] is False
assert "error" in result
@pytest.mark.asyncio
async def test_malformed_input(self, server):
"""Test handling of malformed inputs."""
# Test with various problematic inputs
problematic_inputs = [
None,
123, # Non-string
{"query": "test"}, # Dictionary instead of string
["test", "query"], # List instead of string
]
for bad_input in problematic_inputs:
try:
result = await server.fetch_wikipedia_info(bad_input)
# Should either handle gracefully or raise TypeError
if isinstance(result, dict):
assert "success" in result
if not result["success"]:
assert "error" in result
except (TypeError, AttributeError):
# Acceptable to raise type errors for invalid inputs
pass
class TestUtilities:
"""Test utility functions and helpers."""
@pytest.fixture
def server(self):
return WikipediaServer()
def test_server_initialization(self, server):
"""Test that server initializes correctly."""
assert server is not None
assert hasattr(server, 'fetch_wikipedia_info')
assert hasattr(server, 'list_wikipedia_sections')
assert hasattr(server, 'get_section_content')
@pytest.mark.asyncio
async def test_metadata_consistency(self, server):
"""Test that metadata is consistently included in responses."""
with patch('wikipedia.search') as mock_search, \
patch('wikipedia.page') as mock_page:
# Setup mocks
mock_search.return_value = ["Test Article"]
mock_page_obj = Mock()
mock_page_obj.title = "Test Article"
mock_page_obj.summary = "Test summary"
mock_page_obj.url = "https://test.url"
mock_page_obj.pageid = 12345
mock_page_obj.categories = []
mock_page_obj.links = []
mock_page_obj.sections = ["Overview", "History"]
mock_page_obj.section.return_value = "Section content"
mock_page.return_value = mock_page_obj
# Test all tools include metadata
search_result = await server.fetch_wikipedia_info("Test")
sections_result = await server.list_wikipedia_sections("Test")
content_result = await server.get_section_content("Test", "History")
for result in [search_result, sections_result, content_result]:
assert "metadata" in result
assert "timestamp" in result["metadata"]
# Successful results should have consistent structure
if result["success"]:
assert "data" in result
else:
assert "error" in result
# Integration test that can be run independently
@pytest.mark.asyncio
async def test_full_wikipedia_workflow():
"""Integration test for complete Wikipedia research workflow."""
server = WikipediaServer()
with patch('wikipedia.search') as mock_search, \
patch('wikipedia.page') as mock_page:
# Setup comprehensive mocks
mock_search.return_value = ["Python (programming language)"]
mock_page_obj = Mock()
mock_page_obj.title = "Python (programming language)"
mock_page_obj.summary = "Python is a programming language."
mock_page_obj.url = "https://en.wikipedia.org/wiki/Python_(programming_language)"
mock_page_obj.pageid = 23862
mock_page_obj.categories = ["Programming languages"]
mock_page_obj.links = ["Programming", "Software"]
mock_page_obj.sections = ["Overview", "History", "Features", "Applications"]
mock_page_obj.section.return_value = "Python was created by Guido van Rossum."
mock_page.return_value = mock_page_obj
# Step 1: Search for article
search_result = await server.fetch_wikipedia_info("Python programming")
assert search_result["success"] is True
article_title = search_result["data"]["title"]
# Step 2: Get sections
sections_result = await server.list_wikipedia_sections(article_title)
assert sections_result["success"] is True
sections = sections_result["data"]["sections"]
assert len(sections) > 0
# Step 3: Get content from first section
first_section = sections[0]["title"]
content_result = await server.get_section_content(article_title, first_section)
assert content_result["success"] is True
assert len(content_result["data"]["content"]) > 0
print("✅ Full workflow test completed successfully")
if __name__ == "__main__":
# Allow running tests directly
pytest.main([__file__, "-v"])