Skip to main content
Glama
test_tools_orchestration.py22.6 kB
<<<<<<< Updated upstream import pytest import json from unittest.mock import patch, MagicMock, AsyncMock from datetime import datetime, timedelta from imap_mcp.tools import process_invite_email, identify_meeting_invite, check_calendar_availability, draft_meeting_reply @pytest.fixture def mock_ctx(): """Create a mock context with IMAP and SMTP clients.""" ctx = MagicMock() ctx.log = MagicMock() # Mock IMAP client imap_client = MagicMock() ctx.imap_client = imap_client # Mock SMTP client smtp_client = MagicMock() ctx.smtp_client = smtp_client return ctx @pytest.fixture def mock_email(): """Create a mock email for testing.""" email = MagicMock() email.uid = 12345 email.subject = "Team Meeting - Project Review" email.from_email = "organizer@example.com" email.to = ["recipient@example.com"] email.text = "Let's meet to discuss project progress." email.html = "<p>Let's meet to discuss project progress.</p>" email.folder = "INBOX" email.flags = [] return email @pytest.mark.asyncio async def test_process_invite_success_accept(mock_ctx, mock_email): """Test successful processing of an invite with accept response.""" # Mock identify_meeting_invite mock_identify_result = { "is_invite": True, "invite_details": { "subject": "Team Meeting", "organizer": "organizer@example.com", "start_time": datetime.now().isoformat(), "end_time": (datetime.now() + timedelta(hours=1)).isoformat(), "location": "Conference Room A" }, "email": mock_email } # Set up mocks with patch('imap_mcp.tools.identify_meeting_invite', new_callable=AsyncMock) as mock_identify, \ patch('imap_mcp.tools.check_calendar_availability', new_callable=AsyncMock) as mock_check, \ patch('imap_mcp.tools.draft_meeting_reply', new_callable=AsyncMock) as mock_draft, \ patch('imap_mcp.tools.get_client_from_context') as mock_get_imap, \ patch('imap_mcp.tools.get_smtp_client_from_context') as mock_get_smtp: # Configure mocks mock_identify.return_value = mock_identify_result mock_check.return_value = True # Available mock_draft.return_value = { "reply_subject": "Re: Team Meeting", "reply_body": "I'm confirming my attendance..." } # Mock SMTP client create_reply_mime mock_smtp = MagicMock() mock_smtp.create_reply_mime.return_value = "MIME_MESSAGE_CONTENT" mock_get_smtp.return_value = mock_smtp # Mock IMAP client save_draft_mime mock_imap = MagicMock() mock_imap.save_draft_mime.return_value = 54321 # Draft UID mock_get_imap.return_value = mock_imap # Call the function result = await process_invite_email("INBOX", 12345, mock_ctx) # Verify function calls mock_identify.assert_called_once_with("INBOX", 12345, mock_ctx) mock_check.assert_called_once() mock_draft.assert_called_once() mock_smtp.create_reply_mime.assert_called_once() mock_imap.save_draft_mime.assert_called_once_with("MIME_MESSAGE_CONTENT") # Verify result assert result["status"] == "draft_saved" assert result["draft_uid"] == 54321 assert result["reply_type"] == "accept" assert "summary" in result @pytest.mark.asyncio async def test_process_invite_success_decline(mock_ctx, mock_email): """Test successful processing of an invite with decline response.""" # Mock identify_meeting_invite mock_identify_result = { "is_invite": True, "invite_details": { "subject": "Team Meeting", "organizer": "organizer@example.com", "start_time": datetime.now().isoformat(), "end_time": (datetime.now() + timedelta(hours=1)).isoformat(), "location": "Conference Room A" }, "email": mock_email } # Set up mocks with patch('imap_mcp.tools.identify_meeting_invite', new_callable=AsyncMock) as mock_identify, \ patch('imap_mcp.tools.check_calendar_availability', new_callable=AsyncMock) as mock_check, \ patch('imap_mcp.tools.draft_meeting_reply', new_callable=AsyncMock) as mock_draft, \ patch('imap_mcp.tools.get_client_from_context') as mock_get_imap, \ patch('imap_mcp.tools.get_smtp_client_from_context') as mock_get_smtp: # Configure mocks mock_identify.return_value = mock_identify_result mock_check.return_value = False # Not available mock_draft.return_value = { "reply_subject": "Re: Team Meeting", "reply_body": "Unfortunately, I'm unavailable..." } # Mock SMTP client create_reply_mime mock_smtp = MagicMock() mock_smtp.create_reply_mime.return_value = "MIME_MESSAGE_CONTENT" mock_get_smtp.return_value = mock_smtp # Mock IMAP client save_draft_mime mock_imap = MagicMock() mock_imap.save_draft_mime.return_value = 54321 # Draft UID mock_get_imap.return_value = mock_imap # Call the function result = await process_invite_email("INBOX", 12345, mock_ctx) # Verify result assert result["status"] == "draft_saved" assert result["draft_uid"] == 54321 assert result["reply_type"] == "decline" assert "summary" in result @pytest.mark.asyncio async def test_process_invite_not_invite(mock_ctx): """Test handling of non-invite emails.""" # Mock identify_meeting_invite to return not an invite with patch('imap_mcp.tools.identify_meeting_invite', new_callable=AsyncMock) as mock_identify: mock_identify.return_value = { "is_invite": False, "invite_details": {}, "email": MagicMock() } # Call the function result = await process_invite_email("INBOX", 12345, mock_ctx) # Verify result assert result["status"] == "not_invite" assert "message" in result @pytest.mark.asyncio async def test_process_invite_identify_error(mock_ctx): """Test error handling when identification fails.""" # Mock identify_meeting_invite to raise an exception with patch('imap_mcp.tools.identify_meeting_invite', new_callable=AsyncMock) as mock_identify: mock_identify.side_effect = ValueError("Failed to fetch email") # Call the function result = await process_invite_email("INBOX", 12345, mock_ctx) # Verify result assert result["status"] == "error" assert "Failed to fetch email" in result["message"] @pytest.mark.asyncio async def test_process_invite_calendar_error(mock_ctx, mock_email): """Test error handling when calendar availability check fails.""" # Set up mocks with patch('imap_mcp.tools.identify_meeting_invite', new_callable=AsyncMock) as mock_identify, \ patch('imap_mcp.tools.check_calendar_availability', new_callable=AsyncMock) as mock_check: # Configure mocks mock_identify.return_value = { "is_invite": True, "invite_details": { "subject": "Team Meeting", "organizer": "organizer@example.com", "start_time": datetime.now().isoformat(), "end_time": (datetime.now() + timedelta(hours=1)).isoformat() }, "email": mock_email } mock_check.side_effect = Exception("Calendar API error") # Call the function result = await process_invite_email("INBOX", 12345, mock_ctx) # Verify result assert result["status"] == "error" assert "Calendar API error" in result["message"] @pytest.mark.asyncio async def test_process_invite_draft_reply_error(mock_ctx, mock_email): """Test error handling when drafting reply fails.""" # Set up mocks with patch('imap_mcp.tools.identify_meeting_invite', new_callable=AsyncMock) as mock_identify, \ patch('imap_mcp.tools.check_calendar_availability', new_callable=AsyncMock) as mock_check, \ patch('imap_mcp.tools.draft_meeting_reply', new_callable=AsyncMock) as mock_draft: # Configure mocks mock_identify.return_value = { "is_invite": True, "invite_details": { "subject": "Team Meeting", "organizer": "organizer@example.com", "start_time": datetime.now().isoformat(), "end_time": (datetime.now() + timedelta(hours=1)).isoformat() }, "email": mock_email } mock_check.return_value = True mock_draft.side_effect = ValueError("Missing required fields") # Call the function result = await process_invite_email("INBOX", 12345, mock_ctx) # Verify result assert result["status"] == "error" assert "Missing required fields" in result["message"] @pytest.mark.asyncio async def test_process_invite_save_draft_error(mock_ctx, mock_email): """Test error handling when saving draft fails.""" # Set up mocks with patch('imap_mcp.tools.identify_meeting_invite', new_callable=AsyncMock) as mock_identify, \ patch('imap_mcp.tools.check_calendar_availability', new_callable=AsyncMock) as mock_check, \ patch('imap_mcp.tools.draft_meeting_reply', new_callable=AsyncMock) as mock_draft, \ patch('imap_mcp.tools.get_client_from_context') as mock_get_imap, \ patch('imap_mcp.tools.get_smtp_client_from_context') as mock_get_smtp: # Configure mocks mock_identify.return_value = { "is_invite": True, "invite_details": { "subject": "Team Meeting", "organizer": "organizer@example.com", "start_time": datetime.now().isoformat(), "end_time": (datetime.now() + timedelta(hours=1)).isoformat() }, "email": mock_email } mock_check.return_value = True mock_draft.return_value = { "reply_subject": "Re: Team Meeting", "reply_body": "I'm confirming my attendance..." } # Mock SMTP client mock_smtp = MagicMock() mock_smtp.create_reply_mime.return_value = "MIME_MESSAGE_CONTENT" mock_get_smtp.return_value = mock_smtp # Mock IMAP client to fail when saving draft mock_imap = MagicMock() mock_imap.save_draft_mime.return_value = None # Failed to save mock_get_imap.return_value = mock_imap # Call the function result = await process_invite_email("INBOX", 12345, mock_ctx) # Verify result assert result["status"] == "error" assert "Failed to save draft message" in result["message"] ======= """Tests for the meeting invite orchestration tool.""" import pytest from unittest.mock import patch, MagicMock, AsyncMock from datetime import datetime from email.message import EmailMessage from mcp.server.fastmcp import Context from imap_mcp.models import Email, EmailAddress, EmailContent from imap_mcp.imap_client import ImapClient from imap_mcp.config import ImapConfig from imap_mcp.tools import register_tools class TestMeetingInviteOrchestration: """Tests for the meeting invite orchestration functionality.""" @pytest.fixture def mock_context(self): """Create a mock MCP context.""" ctx = MagicMock(spec=Context) ctx.kwargs = {"client": None} return ctx @pytest.fixture def mock_imap_client(self): """Create a mock IMAP client.""" config = ImapConfig( host="imap.example.com", port=993, username="test@example.com", password="password", use_ssl=True ) client = ImapClient(config) # Mock necessary methods client.fetch_email = MagicMock() client.save_draft_mime = MagicMock() client._get_drafts_folder = MagicMock(return_value="Drafts") return client @pytest.fixture def mock_invite_email(self): """Create a mock meeting invite email.""" return Email( message_id="<invite123@example.com>", subject="Meeting Invitation: Team Sync", from_=EmailAddress(name="Organizer", address="organizer@example.com"), to=[EmailAddress(name="Me", address="me@example.com")], date=datetime(2025, 4, 1, 10, 0, 0), content=EmailContent( text="You are invited to a team sync meeting.\nWhen: Tuesday, April 1, 2025 10:00 AM - 11:00 AM" ), headers={"Content-Type": "text/calendar; method=REQUEST"} ) @pytest.fixture def mock_non_invite_email(self): """Create a mock non-invite email.""" return Email( message_id="<message123@example.com>", subject="Regular Email", from_=EmailAddress(name="Sender", address="sender@example.com"), to=[EmailAddress(name="Me", address="me@example.com")], date=datetime(2025, 4, 1, 9, 0, 0), content=EmailContent( text="This is a regular email, not a meeting invite." ), headers={} ) @pytest.fixture def mock_mcp(self): """Create a mock MCP server.""" mcp = MagicMock() mcp.tool = lambda: lambda func: func return mcp @patch("imap_mcp.workflows.invite_parser.identify_meeting_invite_details") @patch("imap_mcp.workflows.calendar_mock.check_mock_availability") @patch("imap_mcp.workflows.meeting_reply.generate_meeting_reply_content") @patch("imap_mcp.smtp_client.create_reply_mime") @patch("imap_mcp.resources.get_client_from_context") async def test_process_meeting_invite_success( self, mock_get_client, mock_create_reply_mime, mock_generate_reply, mock_check_availability, mock_identify_invite, mock_context, mock_imap_client, mock_invite_email ): """Test successful processing of a meeting invite.""" # Setup mocks mock_get_client.return_value = mock_imap_client mock_imap_client.fetch_email.return_value = mock_invite_email # Mock invite identification mock_identify_invite.return_value = { "is_invite": True, "details": { "subject": "Team Sync", "start_time": datetime(2025, 4, 1, 10, 0, 0), "end_time": datetime(2025, 4, 1, 11, 0, 0), "organizer": "Organizer <organizer@example.com>", "location": "Conference Room" } } # Mock availability check mock_check_availability.return_value = { "available": True, "reason": "Time slot is available", "alternative_times": [] } # Mock reply generation mock_generate_reply.return_value = { "reply_subject": "Accepted: Team Sync", "reply_body": "I'll attend the meeting...", "reply_type": "accept" } # Mock MIME message creation mock_mime_message = MagicMock(spec=EmailMessage) mock_create_reply_mime.return_value = mock_mime_message # Mock draft saving mock_imap_client.save_draft_mime.return_value = 123 # Register tools and get the process_meeting_invite function mock_mcp = self.mock_mcp() register_tools(mock_mcp, mock_imap_client) process_meeting_invite = mock_mcp.tool.return_value # Call the function result = await process_meeting_invite( folder="INBOX", uid=456, ctx=mock_context, availability_mode="always_available" ) # Assertions assert result["status"] == "success" assert "draft_uid" in result assert result["draft_uid"] == 123 assert result["draft_folder"] == "Drafts" assert result["availability"] is True # Verify the mock calls mock_imap_client.fetch_email.assert_called_once_with(456, "INBOX") mock_identify_invite.assert_called_once_with(mock_invite_email) mock_check_availability.assert_called_once() mock_generate_reply.assert_called_once() mock_create_reply_mime.assert_called_once() mock_imap_client.save_draft_mime.assert_called_once_with(mock_mime_message) mock_imap_client._get_drafts_folder.assert_called_once() @patch("imap_mcp.workflows.invite_parser.identify_meeting_invite_details") @patch("imap_mcp.resources.get_client_from_context") async def test_process_non_invite_email( self, mock_get_client, mock_identify_invite, mock_context, mock_imap_client, mock_non_invite_email ): """Test processing a non-invite email.""" # Setup mocks mock_get_client.return_value = mock_imap_client mock_imap_client.fetch_email.return_value = mock_non_invite_email # Mock invite identification mock_identify_invite.return_value = { "is_invite": False, "details": {} } # Register tools and get the process_meeting_invite function mock_mcp = self.mock_mcp() register_tools(mock_mcp, mock_imap_client) process_meeting_invite = mock_mcp.tool.return_value # Call the function result = await process_meeting_invite( folder="INBOX", uid=456, ctx=mock_context ) # Assertions assert result["status"] == "not_invite" assert "The email is not a meeting invite" in result["message"] # Verify the mock calls mock_imap_client.fetch_email.assert_called_once_with(456, "INBOX") mock_identify_invite.assert_called_once_with(mock_non_invite_email) @patch("imap_mcp.resources.get_client_from_context") async def test_process_meeting_invite_email_not_found( self, mock_get_client, mock_context, mock_imap_client ): """Test handling when the email is not found.""" # Setup mocks mock_get_client.return_value = mock_imap_client mock_imap_client.fetch_email.return_value = None # Register tools and get the process_meeting_invite function mock_mcp = self.mock_mcp() register_tools(mock_mcp, mock_imap_client) process_meeting_invite = mock_mcp.tool.return_value # Call the function result = await process_meeting_invite( folder="INBOX", uid=456, ctx=mock_context ) # Assertions assert result["status"] == "error" assert "not found" in result["message"] # Verify the mock calls mock_imap_client.fetch_email.assert_called_once_with(456, "INBOX") @patch("imap_mcp.workflows.invite_parser.identify_meeting_invite_details") @patch("imap_mcp.workflows.calendar_mock.check_mock_availability") @patch("imap_mcp.workflows.meeting_reply.generate_meeting_reply_content") @patch("imap_mcp.smtp_client.create_reply_mime") @patch("imap_mcp.resources.get_client_from_context") async def test_process_meeting_invite_save_draft_failure( self, mock_get_client, mock_create_reply_mime, mock_generate_reply, mock_check_availability, mock_identify_invite, mock_context, mock_imap_client, mock_invite_email ): """Test handling when saving the draft fails.""" # Setup mocks mock_get_client.return_value = mock_imap_client mock_imap_client.fetch_email.return_value = mock_invite_email # Mock invite identification mock_identify_invite.return_value = { "is_invite": True, "details": { "subject": "Team Sync", "start_time": datetime(2025, 4, 1, 10, 0, 0), "end_time": datetime(2025, 4, 1, 11, 0, 0), "organizer": "Organizer <organizer@example.com>", "location": "Conference Room" } } # Mock availability check mock_check_availability.return_value = { "available": False, "reason": "Calendar is busy during this time", "alternative_times": [] } # Mock reply generation mock_generate_reply.return_value = { "reply_subject": "Declined: Team Sync", "reply_body": "I'm unable to attend the meeting...", "reply_type": "decline" } # Mock MIME message creation mock_mime_message = MagicMock(spec=EmailMessage) mock_create_reply_mime.return_value = mock_mime_message # Mock draft saving failure mock_imap_client.save_draft_mime.return_value = None # Register tools and get the process_meeting_invite function mock_mcp = self.mock_mcp() register_tools(mock_mcp, mock_imap_client) process_meeting_invite = mock_mcp.tool.return_value # Call the function result = await process_meeting_invite( folder="INBOX", uid=456, ctx=mock_context, availability_mode="always_busy" ) # Assertions assert result["status"] == "error" assert "Failed to save draft" in result["message"] assert result["availability"] is False # Verify the mock calls mock_imap_client.fetch_email.assert_called_once_with(456, "INBOX") mock_identify_invite.assert_called_once_with(mock_invite_email) mock_check_availability.assert_called_once() mock_generate_reply.assert_called_once() mock_create_reply_mime.assert_called_once() mock_imap_client.save_draft_mime.assert_called_once_with(mock_mime_message) >>>>>>> Stashed changes

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