| | import os |
| | import pytest |
| | import asyncio |
| | import httpx |
| | from unittest.mock import patch, MagicMock, AsyncMock |
| | from dotenv import load_dotenv |
| |
|
| | from stackoverflow_mcp.api import StackExchangeAPI |
| | from stackoverflow_mcp.types import SearchResult |
| |
|
| | |
| | load_dotenv(".env.test") |
| |
|
| |
|
| | @pytest.fixture |
| | def api_key(): |
| | """Return API key from environment or None.""" |
| | return os.getenv("STACK_EXCHANGE_API_KEY") |
| |
|
| |
|
| | @pytest.fixture |
| | def api(api_key): |
| | """Create a StackExchangeAPI instance for testing.""" |
| | api = StackExchangeAPI(api_key=api_key) |
| | yield api |
| | |
| | asyncio.run(api.close()) |
| |
|
| |
|
| | @pytest.fixture |
| | def mock_search_response(): |
| | """Create a mock search response.""" |
| | response = MagicMock() |
| | response.raise_for_status = MagicMock() |
| | response.json = MagicMock(return_value={ |
| | "items": [ |
| | { |
| | "question_id": 12345, |
| | "title": "How to unittest a Flask application?", |
| | "body": "<p>I'm trying to test my Flask application with unittest.</p>", |
| | "score": 25, |
| | "answer_count": 3, |
| | "is_answered": True, |
| | "accepted_answer_id": 54321, |
| | "creation_date": 1609459200, |
| | "last_activity_date": 1609459200, |
| | "view_count": 1000, |
| | "tags": ["python", "flask", "testing", "unittest"], |
| | "link": "https://stackoverflow.com/q/12345", |
| | "closed_date": None, |
| | "owner": { |
| | "user_id": 101, |
| | "display_name": "Test User", |
| | "reputation": 1000 |
| | } |
| | } |
| | ], |
| | "has_more": False, |
| | "quota_max": 300, |
| | "quota_remaining": 299 |
| | }) |
| | return response |
| |
|
| |
|
| | |
| |
|
| | @pytest.mark.asyncio |
| | @pytest.mark.real_api |
| | async def test_search_by_query_real(api): |
| | """Test searching by query using real API.""" |
| | |
| | if not os.getenv("STACK_EXCHANGE_API_KEY"): |
| | pytest.skip("API key required for real API tests") |
| | |
| | results = await api.search_by_query( |
| | query="python unittest flask", |
| | tags=["python", "flask"], |
| | limit=3 |
| | ) |
| | |
| | |
| | assert isinstance(results, list) |
| | if results: |
| | assert isinstance(results[0], SearchResult) |
| | assert results[0].question.title is not None |
| | assert "python" in [tag.lower() for tag in results[0].question.tags] |
| |
|
| |
|
| | @pytest.mark.asyncio |
| | @pytest.mark.real_api |
| | async def test_advanced_search_real(api): |
| | """Test advanced search using real API.""" |
| | |
| | if not os.getenv("STACK_EXCHANGE_API_KEY"): |
| | pytest.skip("API key required for real API tests") |
| | |
| | results = await api.advanced_search( |
| | query="database connection", |
| | tags=["python"], |
| | min_score=10, |
| | has_accepted_answer=True, |
| | limit=2 |
| | ) |
| | |
| | |
| | assert isinstance(results, list) |
| | if results: |
| | assert isinstance(results[0], SearchResult) |
| | assert results[0].question.score >= 10 |
| | assert "python" in [tag.lower() for tag in results[0].question.tags] |
| |
|
| |
|
| | |
| |
|
| | @pytest.mark.asyncio |
| | async def test_search_by_query_mock(api, mock_search_response): |
| | """Test searching by query with mocked response.""" |
| | with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response): |
| | results = await api.search_by_query( |
| | query="flask unittest", |
| | tags=["python", "flask"], |
| | min_score=10, |
| | limit=5 |
| | ) |
| | |
| | assert len(results) == 1 |
| | assert results[0].question.question_id == 12345 |
| | assert results[0].question.title == "How to unittest a Flask application?" |
| | assert "python" in results[0].question.tags |
| | assert "flask" in results[0].question.tags |
| |
|
| |
|
| | @pytest.mark.asyncio |
| | async def test_empty_search_results(api): |
| | """Test empty search results handling.""" |
| | empty_response = MagicMock() |
| | empty_response.raise_for_status = MagicMock() |
| | empty_response.json = MagicMock(return_value={"items": []}) |
| | |
| | with patch.object(httpx.AsyncClient, 'get', return_value=empty_response): |
| | results = await api.search_by_query( |
| | query="definitely will not find anything 89797979", |
| | limit=1 |
| | ) |
| | |
| | assert isinstance(results, list) |
| | assert len(results) == 0 |
| |
|
| |
|
| | @pytest.mark.asyncio |
| | async def test_search_with_min_score_filtering(api, mock_search_response): |
| | """Test that min_score parameter properly filters results.""" |
| | with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response): |
| | |
| | results_included = await api.search_by_query( |
| | query="flask unittest", |
| | min_score=20, |
| | limit=5 |
| | ) |
| | assert len(results_included) == 1 |
| | |
| | |
| | results_filtered = await api.search_by_query( |
| | query="flask unittest", |
| | min_score=30, |
| | limit=5 |
| | ) |
| | assert len(results_filtered) == 0 |
| |
|
| |
|
| | @pytest.mark.asyncio |
| | async def test_search_with_multiple_tags(api, mock_search_response): |
| | """Test searching with multiple tags.""" |
| | with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response): |
| | results = await api.search_by_query( |
| | query="test", |
| | tags=["python", "flask", "django"], |
| | limit=5 |
| | ) |
| | |
| | |
| | call_args = httpx.AsyncClient.get.call_args |
| | params = call_args[1]['params'] |
| | assert 'tagged' in params |
| | assert params['tagged'] == "python;flask;django" |
| |
|
| |
|
| | @pytest.mark.asyncio |
| | async def test_search_with_excluded_tags(api, mock_search_response): |
| | """Test searching with excluded tags.""" |
| | with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response): |
| | results = await api.search_by_query( |
| | query="test", |
| | excluded_tags=["javascript", "c#"], |
| | limit=5 |
| | ) |
| | |
| | |
| | call_args = httpx.AsyncClient.get.call_args |
| | params = call_args[1]['params'] |
| | assert 'nottagged' in params |
| | assert params['nottagged'] == "javascript;c#" |
| |
|
| |
|
| | @pytest.mark.asyncio |
| | async def test_search_with_advanced_parameters(api, mock_search_response): |
| | """Test advanced search with multiple parameters.""" |
| | with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response): |
| | results = await api.advanced_search( |
| | query="flask test", |
| | tags=["python"], |
| | title="unittest", |
| | has_accepted_answer=True, |
| | sort_by="relevance", |
| | limit=5 |
| | ) |
| | |
| | |
| | call_args = httpx.AsyncClient.get.call_args |
| | params = call_args[1]['params'] |
| | assert params['q'] == "flask test" |
| | assert params['tagged'] == "python" |
| | assert params['title'] == "unittest" |
| | assert params['accepted'] == "true" |
| | assert params['sort'] == "relevance" |
| | assert params['pagesize'] == "5" |
| |
|
| |
|
| | @pytest.mark.asyncio |
| | async def test_search_api_error(api): |
| | """Test handling of API errors.""" |
| | error_response = MagicMock() |
| | error_response.raise_for_status = MagicMock( |
| | side_effect=httpx.HTTPStatusError( |
| | "500 Internal Server Error", |
| | request=MagicMock(), |
| | response=MagicMock(status_code=500) |
| | ) |
| | ) |
| | |
| | with patch.object(httpx.AsyncClient, 'get', return_value=error_response): |
| | with pytest.raises(httpx.HTTPStatusError): |
| | await api.search_by_query("test query") |