arcade-mcp/toolkits/google/tests/test_gmail.py
Eric Gustin 83cf070c82
Toolkit lint cleanup (#72)
Included toolkits as part of the linting process.

Cleaned up any tools that needed to be updated because of this.

This portion of the PR description was added via arcade chat!
2024-10-01 10:41:38 -07:00

421 lines
16 KiB
Python

import json
from unittest.mock import MagicMock, patch
import pytest
from arcade_google.tools.gmail import (
delete_draft_email,
list_draft_emails,
list_emails,
list_emails_by_header,
send_draft_email,
send_email,
trash_email,
update_draft_email,
write_draft_email,
)
from arcade_google.tools.utils import parse_draft_email, parse_email
from googleapiclient.errors import HttpError
from arcade.core.errors import ToolExecutionError
from arcade.core.schema import ToolAuthorizationContext, ToolContext
@pytest.fixture
def mock_context():
mock_auth = ToolAuthorizationContext(token="fake-token") # noqa: S106
return ToolContext(authorization=mock_auth)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
async def test_send_email(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Test happy path
result = await send_email(
context=mock_context,
subject="Test Subject",
body="Test Body",
recipient="test@example.com",
)
assert "Email with ID" in result
assert "sent" in result
# Test http error
mock_service.users().messages().send().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Invalid recipient"}}',
)
with pytest.raises(ToolExecutionError):
await send_email(
context=mock_context,
subject="Test Subject",
body="Test Body",
recipient="invalid@example.com",
)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
async def test_write_draft_email(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Test happy path
result = await write_draft_email(
context=mock_context,
subject="Test Draft Subject",
body="Test Draft Body",
recipient="draft@example.com",
)
assert "Draft email with ID" in result
assert "created" in result
# Test http error
mock_service.users().drafts().create().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Invalid request"}}',
)
with pytest.raises(ToolExecutionError):
await write_draft_email(
context=mock_context,
subject="Test Draft Subject",
body="Test Draft Body",
recipient="draft@example.com",
)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
async def test_update_draft_email(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Test happy path
result = await update_draft_email(
context=mock_context,
draft_email_id="draft123",
subject="Updated Subject",
body="Updated Body",
recipient="updated@example.com",
)
assert "Draft email with ID" in result
assert "updated" in result
# Test http error
mock_service.users().drafts().update().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Draft not found"}}',
)
with pytest.raises(ToolExecutionError):
await update_draft_email(
context=mock_context,
draft_email_id="nonexistent_draft",
subject="Updated Subject",
body="Updated Body",
recipient="updated@example.com",
)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
async def test_send_draft_email(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Test happy path
result = await send_draft_email(context=mock_context, email_id="draft456")
assert "Draft email with ID" in result
assert "sent" in result
# Test http error
mock_service.users().drafts().send().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Draft not found"}}',
)
with pytest.raises(ToolExecutionError):
await send_draft_email(context=mock_context, email_id="nonexistent_draft")
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
async def test_delete_draft_email(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Test happy path
result = await delete_draft_email(context=mock_context, draft_email_id="draft789")
assert "Draft email with ID" in result
assert "deleted successfully" in result
# Test http error
mock_service.users().drafts().delete().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Draft not found"}}',
)
with pytest.raises(ToolExecutionError):
await delete_draft_email(context=mock_context, draft_email_id="nonexistent_draft")
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
@patch("arcade_google.tools.gmail.parse_draft_email")
async def test_get_draft_emails(mock_parse_draft_email, mock_build, mock_context):
# Setup test data
mock_drafts_list_response = {
"drafts": [
{
"id": "r9999999999999999999",
"message": {"id": "0000000000000000", "threadId": "0000000000000000"},
}
],
"resultSizeEstimate": 1,
}
mock_drafts_get_response = {
"id": "r9999999999999999999",
"message": {
"id": "0000000000000000",
"threadId": "0000000000000000",
"labelIds": ["DRAFT"],
"snippet": "Hello! This is a test. Best regards, John",
"payload": {
"partId": "",
"mimeType": "text/plain",
"filename": "",
"headers": [
{"name": "to", "value": "test@arcade-ai.com"},
{"name": "subject", "value": "New Draft"},
{"name": "Date", "value": "Mon, 16 Sep 2024 13:02:10 -0400"},
{"name": "From", "value": "john-doe@arcade-ai.com"},
],
"body": {
"size": 41,
"data": "SGVsbG8hIFRoaXMgaXMgYSB0ZXN0LgoKQmVzdCByZWdhcmRzLApCb2I=",
},
},
"sizeEstimate": 453,
"historyId": "7061",
"internalDate": "1726506130000",
},
}
# Setup mocking
mock_service = MagicMock()
mock_build.return_value = mock_service
# Mock the response from the Gmail list drafts API
mock_service.users().drafts().list().execute.return_value = mock_drafts_list_response
# Mock the response from the Gmail get drafts API
mock_service.users().drafts().get().execute.return_value = mock_drafts_get_response
# Mock the parse_email function since parse_email doesn't accept object of type MagicMock
mock_parse_draft_email.return_value = parse_draft_email(mock_drafts_get_response)
# Test happy path
result = await list_draft_emails(context=mock_context, n_drafts=2)
assert isinstance(result, str)
result_json = json.loads(result)
assert isinstance(result_json, dict)
assert "emails" in result_json
assert len(result_json["emails"]) == 1
assert all("id" in draft and "subject" in draft for draft in result_json["emails"])
# Test http error
mock_service.users().drafts().list().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Invalid request"}}',
)
with pytest.raises(ToolExecutionError):
await list_draft_emails(context=mock_context, n_drafts=2)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
@patch("arcade_google.tools.gmail.parse_email")
async def test_search_emails_by_header(mock_parse_email, mock_build, mock_context):
# Setup test data
mock_messages_list_response = {
"messages": [
{"id": "191fbc8ddce0f433", "threadId": "191fbc8ddce0f433"},
{"id": "191fbc0ea11efa90", "threadId": "191fbc0ea11efa90"},
],
"nextPageToken": "00755945214480102915",
"resultSizeEstimate": 201,
}
mock_messages_get_response = {
"id": "191f2cf4d24bf23d",
"threadId": "191f2cf4d24bf23d",
"labelIds": ["UNREAD", "IMPORTANT", "CATEGORY_UPDATES", "INBOX"],
"snippet": "Hey User, Your personal access token (classic) "ArcadeAI" with admin:enterprise, admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, admin:ssh_signing_key,",
"payload": {
"partId": "",
"mimeType": "text/plain",
"filename": "",
"headers": [
{"name": "Delivered-To", "value": "example@arcade-ai.com"},
{"name": "Date", "value": "Sat, 14 Sep 2024 16:12:37 -0700"},
{"name": "From", "value": "GitHub \u003cnoreply@github.com\u003e"},
{"name": "To", "value": "example@arcade-ai.com"},
{
"name": "Subject",
"value": "[GitHub] Your personal access token (classic) has expired",
},
],
"body": {
"size": 605,
"data": "SGV5IEBFcmljR3VzdGluLA0KDQpZb3VyIHBlcnNvbmFsIGFjY2VzcyB0b2tlbiAoY2xhc3NpYykgIkFyY2FkZUFJIiB3aXRoIGFkbWluOmVudGVycHJpc2UsIGFkbWluOmdwZ19rZXksIGFkbWluOm9yZywgYWRtaW46b3JnX2hvb2ssIGFkbWluOnB1YmxpY19rZXksIGFkbWluOnJlcG9faG9vaywgYWRtaW46c3NoX3NpZ25pbmdfa2V5LCBhdWRpdF9sb2csIGNvZGVzcGFjZSwgY29waWxvdCwgZGVsZXRlOnBhY2thZ2VzLCBkZWxldGVfcmVwbywgZ2lzdCwgbm90aWZpY2F0aW9ucywgcHJvamVjdCwgcmVwbywgdXNlciwgd29ya2Zsb3csIHdyaXRlOmRpc2N1c3Npb24sIGFuZCB3cml0ZTpwYWNrYWdlcyBzY29wZXMgaGFzIGV4cGlyZWQuDQoNCklmIHRoaXMgdG9rZW4gaXMgc3RpbGwgbmVlZGVkLCB2aXNpdCBodHRwczovL2dpdGh1Yi5jb20vc2V0dGluZ3MvdG9rZW5zLzE3MTM2OTg2MTMvcmVnZW5lcmF0ZSB0byBnZW5lcmF0ZSBhbiBlcXVpdmFsZW50Lg0KDQpJZiB5b3UgcnVuIGludG8gcHJvYmxlbXMsIHBsZWFzZSBjb250YWN0IHN1cHBvcnQgYnkgdmlzaXRpbmcgaHR0cHM6Ly9naXRodWIuY29tL2NvbnRhY3QNCg0KVGhhbmtzLA0KVGhlIEdpdEh1YiBUZWFtDQo=",
},
},
"sizeEstimate": 4512,
"historyId": "5508",
"internalDate": "1726355557000",
}
# Setup mocking
mock_service = MagicMock()
mock_build.return_value = mock_service
# Mock the response from the Gmail list messages API
mock_service.users().messages().list().execute.return_value = mock_messages_list_response
# Mock the response from the Gmail get messages API
mock_service.users().messages().get().execute.return_value = mock_messages_get_response
# Mock the parse_email function since parse_email doesn't accept object of type MagicMock
mock_parse_email.return_value = parse_email(mock_messages_get_response)
# Test happy path
result = await list_emails_by_header(context=mock_context, sender="noreply@github.com", limit=2)
assert isinstance(result, str)
result_json = json.loads(result)
assert isinstance(result_json, dict)
assert "emails" in result_json
assert len(result_json["emails"]) == 2
assert all("id" in email and "subject" in email for email in result_json["emails"])
# Test http error
mock_service.users().messages().list().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Invalid request"}}',
)
with pytest.raises(ToolExecutionError):
await list_emails_by_header(context=mock_context, sender="noreply@github.com", limit=2)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
@patch("arcade_google.tools.gmail.parse_email")
async def test_get_emails(mock_parse_email, mock_build, mock_context):
# Setup test data
mock_messages_list_response = {
"messages": [
{"id": "191fbc8ddce0f433", "threadId": "191fbc8ddce0f433"},
],
"nextPageToken": "00755945214480102915",
"resultSizeEstimate": 1,
}
mock_messages_get_response = {
"id": "191f2cf4d24bf23d",
"threadId": "191f2cf4d24bf23d",
"labelIds": ["UNREAD", "IMPORTANT", "CATEGORY_UPDATES", "INBOX"],
"snippet": "Hey User, Your personal access token (classic) "ArcadeAI" with admin:enterprise, admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, admin:ssh_signing_key,",
"payload": {
"partId": "",
"mimeType": "text/plain",
"filename": "",
"headers": [
{"name": "Delivered-To", "value": "example@arcade-ai.com"},
{"name": "Date", "value": "Sat, 14 Sep 2024 16:12:37 -0700"},
{"name": "From", "value": "GitHub \u003cnoreply@github.com\u003e"},
{"name": "To", "value": "example@arcade-ai.com"},
{
"name": "Subject",
"value": "[GitHub] Your personal access token (classic) has expired",
},
],
"body": {
"size": 605,
"data": "SGV5IEBFcmljR3VzdGluLA0KDQpZb3VyIHBlcnNvbmFsIGFjY2VzcyB0b2tlbiAoY2xhc3NpYykgIkFyY2FkZUFJIiB3aXRoIGFkbWluOmVudGVycHJpc2UsIGFkbWluOmdwZ19rZXksIGFkbWluOm9yZywgYWRtaW46b3JnX2hvb2ssIGFkbWluOnB1YmxpY19rZXksIGFkbWluOnJlcG9faG9vaywgYWRtaW46c3NoX3NpZ25pbmdfa2V5LCBhdWRpdF9sb2csIGNvZGVzcGFjZSwgY29waWxvdCwgZGVsZXRlOnBhY2thZ2VzLCBkZWxldGVfcmVwbywgZ2lzdCwgbm90aWZpY2F0aW9ucywgcHJvamVjdCwgcmVwbywgdXNlciwgd29ya2Zsb3csIHdyaXRlOmRpc2N1c3Npb24sIGFuZCB3cml0ZTpwYWNrYWdlcyBzY29wZXMgaGFzIGV4cGlyZWQuDQoNCklmIHRoaXMgdG9rZW4gaXMgc3RpbGwgbmVlZGVkLCB2aXNpdCBodHRwczovL2dpdGh1Yi5jb20vc2V0dGluZ3MvdG9rZW5zLzE3MTM2OTg2MTMvcmVnZW5lcmF0ZSB0byBnZW5lcmF0ZSBhbiBlcXVpdmFsZW50Lg0KDQpJZiB5b3UgcnVuIGludG8gcHJvYmxlbXMsIHBsZWFzZSBjb250YWN0IHN1cHBvcnQgYnkgdmlzaXRpbmcgaHR0cHM6Ly9naXRodWIuY29tL2NvbnRhY3QNCg0KVGhhbmtzLA0KVGhlIEdpdEh1YiBUZWFtDQo=",
},
},
"sizeEstimate": 4512,
"historyId": "5508",
"internalDate": "1726355557000",
}
# Setup mocking
mock_service = MagicMock()
mock_build.return_value = mock_service
# Mock the response from the Gmail list messages API
mock_service.users().messages().list().execute.return_value = mock_messages_list_response
# Mock the Gmail get messages API
mock_service.users().messages().get().execute.return_value = mock_messages_get_response
# Mock the parse_email function since parse_email doesn't accept object of type MagicMock
mock_parse_email.return_value = parse_email(mock_messages_get_response)
# Test happy path
result = await list_emails(context=mock_context, n_emails=1)
# Assert the result
assert isinstance(result, str)
result_json = json.loads(result)
assert isinstance(result_json, dict)
assert "emails" in result_json
assert len(result_json["emails"]) == 1
assert "id" in result_json["emails"][0]
assert "subject" in result_json["emails"][0]
assert "date" in result_json["emails"][0]
assert "body" in result_json["emails"][0]
# Test http error
mock_service.users().messages().list().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Invalid request"}}',
)
with pytest.raises(ToolExecutionError):
await list_emails(context=mock_context, n_emails=1)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail.build")
async def test_trash_email(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Test happy path
email_id = "123456"
result = await trash_email(context=mock_context, email_id=email_id)
assert (
f"Email with ID {email_id} trashed successfully: https://mail.google.com/mail/u/0/#trash/{email_id}"
== result
)
# Test http error
mock_service.users().messages().trash().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Email not found"}}',
)
with pytest.raises(ToolExecutionError):
await trash_email(context=mock_context, email_id="nonexistent_email")