Skip to main content
Glama
test_tools.py13.6 kB
"""Tests for MCP tools implementation.""" import json import pytest from unittest.mock import MagicMock, patch from datetime import datetime from mcp.server.fastmcp import FastMCP, Context from imap_mcp.imap_client import ImapClient from imap_mcp.models import Email, EmailAddress, EmailContent from imap_mcp.tools import register_tools # Patch the get_client_from_context function to use our mock client @pytest.fixture(autouse=True) def patch_get_client(): with patch('imap_mcp.tools.get_client_from_context') as mock_get_client: yield mock_get_client class TestTools: """Test class for MCP tools.""" @pytest.fixture def mock_email(self): """Create a mock email object.""" email = Email( message_id="<test123@example.com>", subject="Test Email", from_=EmailAddress(name="Sender", address="sender@example.com"), to=[EmailAddress(name="Recipient", address="recipient@example.com")], cc=[], bcc=[], date=datetime.now(), content=EmailContent(text="Test content", html="<p>Test content</p>"), attachments=[], flags=["\\Seen"], headers={}, folder="INBOX", uid=1 ) return email @pytest.fixture def mock_client(self, mock_email): """Create a mock IMAP client.""" client = MagicMock(spec=ImapClient) # Configure default return values client.move_email.return_value = True client.mark_email.return_value = True client.delete_email.return_value = True client.list_folders.return_value = ["INBOX", "Sent", "Archive", "Trash"] client.search.return_value = [1, 2, 3] client.fetch_emails.return_value = {1: mock_email, 2: mock_email, 3: mock_email} client.fetch_email.return_value = mock_email return client @pytest.fixture def tools(self, mock_client): """Set up tools for testing.""" # Create a mock MCP server mcp = MagicMock(spec=FastMCP) # Make tool decorator store and return the decorated function stored_tools = {} def mock_tool_decorator(): def decorator(func): stored_tools[func.__name__] = func return func return decorator mcp.tool = mock_tool_decorator # Register tools with our mock register_tools(mcp, mock_client) # Return the tools dictionary return stored_tools @pytest.fixture def mock_context(self, mock_client, patch_get_client): """Create a mock context and configure get_client_from_context.""" context = MagicMock(spec=Context) patch_get_client.return_value = mock_client return context @pytest.mark.asyncio async def test_move_email(self, tools, mock_client, mock_context): """Test moving an email from one folder to another.""" # Get the move_email function move_email = tools["move_email"] # Call the move_email function result = await move_email("INBOX", 123, "Archive", mock_context) # Check the client was called correctly mock_client.move_email.assert_called_once_with(123, "INBOX", "Archive") # Check the result assert "Email moved from INBOX to Archive" in result # Test error handling mock_client.move_email.side_effect = Exception("Connection error") result = await move_email("INBOX", 123, "Archive", mock_context) assert "Error" in result @pytest.mark.asyncio async def test_mark_as_read(self, tools, mock_client, mock_context): """Test marking an email as read.""" # Get the mark_as_read function mark_as_read = tools["mark_as_read"] # Call the function result = await mark_as_read("INBOX", 123, mock_context) # Check the client was called correctly mock_client.mark_email.assert_called_once_with(123, "INBOX", "\\Seen", True) # Check the result assert "Email marked as read" in result # Test failure case mock_client.mark_email.return_value = False result = await mark_as_read("INBOX", 123, mock_context) assert "Failed to mark email as read" in result @pytest.mark.asyncio async def test_mark_as_unread(self, tools, mock_client, mock_context): """Test marking an email as unread.""" # Get the mark_as_unread function mark_as_unread = tools["mark_as_unread"] # Reset mock for this test mock_client.mark_email.reset_mock() mock_client.mark_email.return_value = True # Call the function result = await mark_as_unread("INBOX", 123, mock_context) # Check the client was called correctly mock_client.mark_email.assert_called_once_with(123, "INBOX", "\\Seen", False) # Check the result assert "Email marked as unread" in result # Test error handling mock_client.mark_email.side_effect = Exception("Server error") result = await mark_as_unread("INBOX", 123, mock_context) assert "Error" in result @pytest.mark.asyncio async def test_flag_email(self, tools, mock_client, mock_context): """Test flagging and unflagging an email.""" # Get the flag_email function flag_email = tools["flag_email"] # Reset mock for this test mock_client.mark_email.reset_mock() mock_client.mark_email.return_value = True # Test flagging result = await flag_email("INBOX", 123, mock_context, True) mock_client.mark_email.assert_called_once_with(123, "INBOX", "\\Flagged", True) assert "Email flagged" in result # Reset mock mock_client.mark_email.reset_mock() # Test unflagging result = await flag_email("INBOX", 123, mock_context, False) mock_client.mark_email.assert_called_once_with(123, "INBOX", "\\Flagged", False) assert "Email unflagged" in result @pytest.mark.asyncio async def test_delete_email(self, tools, mock_client, mock_context): """Test deleting an email.""" # Get the delete_email function delete_email = tools["delete_email"] # Call the function result = await delete_email("INBOX", 123, mock_context) # Check the client was called correctly mock_client.delete_email.assert_called_once_with(123, "INBOX") # Check the result assert "Email deleted" in result # Test failure case mock_client.delete_email.return_value = False result = await delete_email("INBOX", 123, mock_context) assert "Failed to delete" in result # Test error handling mock_client.delete_email.side_effect = Exception("Permission denied") result = await delete_email("INBOX", 123, mock_context) assert "Error" in result @pytest.mark.asyncio async def test_search_emails(self, tools, mock_client, mock_context, mock_email): """Test searching for emails.""" # Get the search_emails function search_emails = tools["search_emails"] # Test searching with default parameters result = await search_emails("test query", mock_context) result_data = json.loads(result) # Assert client methods were called properly mock_client.list_folders.assert_called_once() assert mock_client.search.call_count > 0 # Check result structure assert isinstance(result_data, list) assert len(result_data) > 0 assert "uid" in result_data[0] assert "folder" in result_data[0] assert "subject" in result_data[0] # Reset mocks mock_client.list_folders.reset_mock() mock_client.search.reset_mock() mock_client.fetch_emails.reset_mock() # Test searching with specific folder result = await search_emails("test query", mock_context, folder="INBOX") # Assert client methods were called properly mock_client.list_folders.assert_not_called() mock_client.search.assert_called_once() # Test with different criteria criteria_tests = ["from", "to", "subject", "all", "unseen", "seen"] for criteria in criteria_tests: mock_client.search.reset_mock() result = await search_emails("test query", mock_context, criteria=criteria) assert mock_client.search.called # Test with invalid criteria result = await search_emails("test query", mock_context, criteria="invalid") assert "Invalid search criteria" in result @pytest.mark.asyncio async def test_process_email(self, tools, mock_client, mock_context): """Test processing an email with multiple actions.""" # Get the process_email function process_email = tools["process_email"] # Test move action mock_client.move_email.reset_mock() mock_client.move_email.return_value = True result = await process_email( "INBOX", 123, "move", mock_context, target_folder="Archive" ) mock_client.move_email.assert_called_once_with(123, "INBOX", "Archive") assert "Email moved" in result # Test move action without target folder result = await process_email("INBOX", 123, "move", mock_context) assert "Target folder must be specified" in result # Test read action mock_client.mark_email.reset_mock() mock_client.mark_email.return_value = True result = await process_email("INBOX", 123, "read", mock_context) mock_client.mark_email.assert_called_once_with(123, "INBOX", "\\Seen", True) assert "Email marked as read" in result # Test unread action mock_client.mark_email.reset_mock() mock_client.mark_email.return_value = True result = await process_email("INBOX", 123, "unread", mock_context) mock_client.mark_email.assert_called_once_with(123, "INBOX", "\\Seen", False) assert "Email marked as unread" in result # Test flag action mock_client.mark_email.reset_mock() mock_client.mark_email.return_value = True result = await process_email("INBOX", 123, "flag", mock_context) mock_client.mark_email.assert_called_once_with(123, "INBOX", "\\Flagged", True) assert "Email flagged" in result # Test delete action mock_client.delete_email.reset_mock() mock_client.delete_email.return_value = True result = await process_email("INBOX", 123, "delete", mock_context) mock_client.delete_email.assert_called_once_with(123, "INBOX") assert "Email deleted" in result # Test invalid action result = await process_email("INBOX", 123, "invalid_action", mock_context) assert "Invalid action" in result # Test email not found mock_client.fetch_email.return_value = None result = await process_email("INBOX", 123, "read", mock_context) assert "not found" in result @pytest.mark.asyncio async def test_tool_error_handling(self, tools, mock_client, mock_context): """Test error handling in tools.""" # Get tools to test move_email = tools["move_email"] mark_as_read = tools["mark_as_read"] search_emails = tools["search_emails"] # Test move_email error handling mock_client.move_email.side_effect = Exception("Network error") result = await move_email("INBOX", 123, "Archive", mock_context) assert "Error" in result # Test mark_as_read error handling mock_client.mark_email.side_effect = Exception("Server timeout") result = await mark_as_read("INBOX", 123, mock_context) assert "Error" in result # Test search_emails error handling mock_client.search.side_effect = Exception("Search failed") result = await search_emails("test", mock_context) # Search should continue with other folders and return an empty list assert "[]" in result or result == "[]" @pytest.mark.asyncio async def test_tool_parameter_validation(self, tools, mock_client, mock_context): """Test parameter validation in tools.""" # Get tools to test search_emails = tools["search_emails"] process_email = tools["process_email"] # Test search_emails with invalid criteria result = await search_emails("test", mock_context, criteria="invalid_criteria") assert "Invalid search criteria" in result # Test process_email with missing target folder for move action result = await process_email("INBOX", 123, "move", ctx=mock_context) assert "Target folder must be specified" in result # Test process_email with invalid action result = await process_email("INBOX", 123, "nonexistent_action", ctx=mock_context) assert "Invalid action" in result

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/non-dirty/imap-mcp'

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