arcade-mcp/toolkits/google/tests/test_gmail.py
Eric Gustin 81ada1d9b9
Add Google File Picker Tool (#361)
## Google File Picker
The Google Picker lets users select or upload Google Drive files. Users
can grant permission to your apps to access their Drive data, providing
a secure and authorized way to interact with their files.

The `generate_google_file_picker_url` returns a URL to a Google File
Picker for the user.
2025-04-18 18:25:05 -07:00

951 lines
33 KiB
Python

from base64 import urlsafe_b64encode
from email.message import EmailMessage
from unittest.mock import MagicMock, patch
import pytest
from arcade.sdk import ToolAuthorizationContext, ToolContext
from arcade.sdk.errors import ToolExecutionError
from googleapiclient.errors import HttpError
from arcade_google.models import GmailReplyToWhom
from arcade_google.tools import (
delete_draft_email,
get_thread,
list_draft_emails,
list_emails,
list_emails_by_header,
list_threads,
reply_to_email,
search_threads,
send_draft_email,
send_email,
trash_email,
update_draft_email,
write_draft_email,
)
from arcade_google.utils import (
build_reply_body,
parse_draft_email,
parse_multipart_email,
parse_plain_text_email,
)
@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_gmail_service")
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 isinstance(result, dict)
assert "id" in result
assert "thread_id" in result
assert "subject" in result
assert "body" 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_gmail_service")
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 isinstance(result, dict)
assert "id" in result
assert "thread_id" in result
assert "subject" in result
assert "body" 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_gmail_service")
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 isinstance(result, dict)
assert "id" in result
assert "thread_id" in result
assert "subject" in result
assert "body" 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_gmail_service")
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 isinstance(result, dict)
assert "id" in result
assert "thread_id" in result
assert "subject" in result
assert "body" 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_gmail_service")
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_gmail_service")
@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_draft_email function since parse_draft_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, dict)
assert "emails" in result
assert len(result["emails"]) == 1
assert all("id" in draft and "subject" in draft for draft in result["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_gmail_service")
@patch("arcade_google.tools.gmail.parse_plain_text_email")
async def test_search_emails_by_header(mock_parse_plain_text_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_plain_text_email function since parse_plain_text_email doesn't accept object of type MagicMock
mock_parse_plain_text_email.return_value = parse_plain_text_email(mock_messages_get_response)
# Test happy path
result = await list_emails_by_header(
context=mock_context, sender="noreply@github.com", max_results=2
)
assert isinstance(result, dict)
assert "emails" in result
assert len(result["emails"]) == 2
assert all("id" in email and "subject" in email for email in result["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", max_results=2
)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail._build_gmail_service")
@patch("arcade_google.tools.gmail.parse_plain_text_email")
async def test_get_emails(mock_parse_plain_text_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_plain_text_email function since parse_plain_text_email doesn't accept object of type MagicMock
mock_parse_plain_text_email.return_value = parse_plain_text_email(mock_messages_get_response)
# Test happy path
result = await list_emails(context=mock_context, n_emails=1)
assert isinstance(result, dict)
assert "emails" in result
assert len(result["emails"]) == 1
assert "id" in result["emails"][0]
assert "subject" in result["emails"][0]
assert "date" in result["emails"][0]
assert "body" in result["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_gmail_service")
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 isinstance(result, dict)
assert "id" in result
assert "thread_id" in result
assert "subject" in result
assert "body" in 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")
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail._build_gmail_service")
async def test_search_threads(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Setup mock response data
mock_threads_list_response = {
"threads": [
{
"id": "thread1",
"snippet": "Thread snippet 1",
},
{
"id": "thread2",
"snippet": "Thread snippet 2",
},
],
"nextPageToken": "next_token_123",
"resultSizeEstimate": 2,
}
# Mock the Gmail API threads().list() method
mock_service.users().threads().list().execute.return_value = mock_threads_list_response
# Test happy path
result = await search_threads(
context=mock_context,
sender="test@example.com",
max_results=2,
)
assert isinstance(result, dict)
assert "threads" in result
assert len(result["threads"]) == 2
assert result["threads"][0]["id"] == "thread1"
assert "next_page_token" in result
# Test error handling
mock_service.users().threads().list().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Invalid request"}}',
)
with pytest.raises(ToolExecutionError):
await search_threads(
context=mock_context,
sender="test@example.com",
max_results=2,
)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail._build_gmail_service")
async def test_list_threads(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Setup mock response data
mock_threads_list_response = {
"threads": [
{
"id": "thread1",
"snippet": "Thread snippet 1",
},
{
"id": "thread2",
"snippet": "Thread snippet 2",
},
],
"nextPageToken": "next_token_123",
"resultSizeEstimate": 2,
}
# Mock the Gmail API threads().list() method
mock_service.users().threads().list().execute.return_value = mock_threads_list_response
# Test happy path
result = await list_threads(
context=mock_context,
max_results=2,
)
assert isinstance(result, dict)
assert "threads" in result
assert len(result["threads"]) == 2
assert result["threads"][0]["id"] == "thread1"
assert "next_page_token" in result
# Test error handling
mock_service.users().threads().list().execute.side_effect = HttpError(
resp=MagicMock(status=400),
content=b'{"error": {"message": "Invalid request"}}',
)
with pytest.raises(ToolExecutionError):
await list_threads(
context=mock_context,
max_results=2,
)
@pytest.mark.asyncio
@patch("arcade_google.tools.gmail._build_gmail_service")
async def test_get_thread(mock_build, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
# Setup mock response data
mock_thread_get_response = {
"id": "thread1",
"messages": [
{
"id": "message1",
"snippet": "Message snippet 1",
},
{
"id": "message2",
"snippet": "Message snippet 2",
},
],
}
# Mock the Gmail API threads().get() method
mock_service.users().threads().get().execute.return_value = mock_thread_get_response
# Test happy path
result = await get_thread(
context=mock_context,
thread_id="thread1",
)
assert isinstance(result, dict)
assert "id" in result
assert result["id"] == "thread1"
assert "messages" in result
assert len(result["messages"]) == 2
assert result["messages"][0]["id"] == "message1"
# Test error handling
mock_service.users().threads().get().execute.side_effect = HttpError(
resp=MagicMock(status=404),
content=b'{"error": {"message": "Thread not found"}}',
)
with pytest.raises(ToolExecutionError):
await get_thread(
context=mock_context,
thread_id="invalid_thread",
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"reply_to_whom, expected_to, expected_cc",
[
(
GmailReplyToWhom.EVERY_RECIPIENT,
"sender@example.com, to1@example.com, to2@example.com",
"cc1@example.com, cc2@example.com",
),
(
GmailReplyToWhom.ONLY_THE_SENDER,
"sender@example.com",
"",
),
],
)
@patch("arcade_google.tools.gmail._build_gmail_service")
async def test_reply_to_email(mock_build, reply_to_whom, expected_to, expected_cc, mock_context):
mock_service = MagicMock()
mock_build.return_value = mock_service
original_message = {
"id": "id123456",
"threadId": "thread123456",
"payload": {
"headers": [
{"name": "Message-ID", "value": "id123456"},
{"name": "Subject", "value": "test"},
{"name": "From", "value": "sender@example.com"},
{"name": "To", "value": "to1@example.com, to2@example.com, test@example.com"},
{"name": "Cc", "value": "cc1@example.com, cc2@example.com"},
{"name": "References", "value": "thread123456"},
],
},
}
mock_service.users().getProfile().execute.return_value = {"emailAddress": "test@example.com"}
mock_service.users().messages().get().execute.return_value = original_message
result = await reply_to_email(
context=mock_context,
body="test",
reply_to_message_id="id123456",
reply_to_whom=reply_to_whom,
)
assert isinstance(result, dict)
assert "url" in result
replying_to = parse_multipart_email(original_message)
expected_body = build_reply_body("test", replying_to)
expected_message = EmailMessage()
expected_message.set_content(expected_body)
expected_message["To"] = expected_to
expected_message["Subject"] = "Re: test"
if expected_cc:
expected_message["Cc"] = expected_cc
expected_message["In-Reply-To"] = "id123456"
expected_message["References"] = "id123456, thread123456"
mock_service.users().messages().send.assert_called_once_with(
userId="me",
body={
"raw": urlsafe_b64encode(expected_message.as_bytes()).decode(),
"threadId": "thread123456",
},
)
def test_parse_multipart_email_full():
"""
Test parsing a multipart email with both plain text and HTML bodies.
"""
email_data = {
"id": "email123",
"threadId": "thread123",
"labelIds": ["INBOX", "UNREAD"],
"historyId": "history123",
"snippet": "This is a test email.",
"payload": {
"headers": [
{"name": "To", "value": "recipient@example.com"},
{"name": "From", "value": "sender@example.com"},
{"name": "Subject", "value": "Test Email"},
{"name": "Date", "value": "Mon, 1 Jan 2024 10:00:00 -0000"},
],
"body": {"size": 100, "data": "VGhpcyBpcyBhIHRlc3QgZW1haWwu"},
},
}
with (
patch("arcade_google.utils._get_email_plain_text_body") as mock_plain,
patch("arcade_google.utils._get_email_html_body") as mock_html,
patch("arcade_google.utils._clean_email_body") as mock_clean,
):
# Mock the helper functions
mock_plain.return_value = "This is a test email."
mock_html.return_value = "<p>This is a test email.</p>"
mock_clean.return_value = "This is a test email."
result = parse_multipart_email(email_data)
assert result["id"] == "email123"
assert result["thread_id"] == "thread123"
assert result["label_ids"] == ["INBOX", "UNREAD"]
assert result["snippet"] == "This is a test email."
assert result["to"] == "recipient@example.com"
assert result["from"] == "sender@example.com"
assert result["subject"] == "Test Email"
assert result["date"] == "Mon, 1 Jan 2024 10:00:00 -0000"
assert result["plain_text_body"] == "This is a test email."
assert result["html_body"] == "<p>This is a test email.</p>"
def test_parse_multipart_email_plain_only():
"""
Test parsing an email with only a plain text body.
"""
email_data = {
"id": "email456",
"threadId": "thread456",
"labelIds": ["INBOX"],
"historyId": "history456",
"snippet": "Plain text only email.",
"payload": {
"headers": [
{"name": "To", "value": "recipient2@example.com"},
{"name": "From", "value": "sender2@example.com"},
{"name": "Subject", "value": "Plain Text Email"},
{"name": "Date", "value": "Tue, 2 Feb 2024 11:00:00 -0000"},
],
"body": {"size": 150, "data": "UGxhaW4gdGV4dCBvbmx5IGVtYWlsLg=="},
},
}
with (
patch("arcade_google.utils._get_email_plain_text_body") as mock_plain,
patch("arcade_google.utils._get_email_html_body") as mock_html,
patch("arcade_google.utils._clean_email_body") as mock_clean,
):
# Mock the helper functions
mock_plain.return_value = "Plain text only email."
mock_html.return_value = None
mock_clean.return_value = "Plain text only email."
result = parse_multipart_email(email_data)
assert result["id"] == "email456"
assert result["thread_id"] == "thread456"
assert result["label_ids"] == ["INBOX"]
assert result["snippet"] == "Plain text only email."
assert result["to"] == "recipient2@example.com"
assert result["from"] == "sender2@example.com"
assert result["subject"] == "Plain Text Email"
assert result["date"] == "Tue, 2 Feb 2024 11:00:00 -0000"
assert result["plain_text_body"] == "Plain text only email."
assert result["html_body"] == ""
def test_parse_multipart_email_html_only():
"""
Test parsing an email with only an HTML body.
"""
email_data = {
"id": "email789",
"threadId": "thread789",
"labelIds": ["SENT"],
"historyId": "history789",
"snippet": "HTML only email.",
"payload": {
"headers": [
{"name": "To", "value": "recipient3@example.com"},
{"name": "From", "value": "sender3@example.com"},
{"name": "Subject", "value": "HTML Email"},
{"name": "Date", "value": "Wed, 3 Mar 2024 12:00:00 -0000"},
],
"body": {"size": 200, "data": "PGh0bWw+VGhpcyBpcyBIVE1MIGVtYWlsLjwvaHRtbD4="},
},
}
with (
patch("arcade_google.utils._get_email_plain_text_body") as mock_plain,
patch("arcade_google.utils._get_email_html_body") as mock_html,
patch("arcade_google.utils._clean_email_body") as mock_clean,
):
# Mock the helper functions
mock_plain.return_value = None
mock_html.return_value = "<html>This is HTML email.</html>"
mock_clean.return_value = "This is HTML email."
result = parse_multipart_email(email_data)
assert result["id"] == "email789"
assert result["thread_id"] == "thread789"
assert result["label_ids"] == ["SENT"]
assert result["snippet"] == "HTML only email."
assert result["to"] == "recipient3@example.com"
assert result["from"] == "sender3@example.com"
assert result["subject"] == "HTML Email"
assert result["date"] == "Wed, 3 Mar 2024 12:00:00 -0000"
assert result["plain_text_body"] == "This is HTML email."
assert result["html_body"] == "<html>This is HTML email.</html>"
def test_parse_multipart_email_missing_payload():
"""
Test parsing an email with missing payload.
"""
email_data = {
"id": "email000",
"threadId": "thread000",
"labelIds": ["INBOX"],
"historyId": "history000",
"snippet": "Missing payload email.",
# 'payload' key is missing
}
result = parse_multipart_email(email_data)
# Since payload is missing, headers and bodies should be default or empty
assert result["id"] == "email000"
assert result["thread_id"] == "thread000"
assert result["label_ids"] == ["INBOX"]
assert result["snippet"] == "Missing payload email."
assert result["to"] == ""
assert result["from"] == ""
assert result["subject"] == ""
assert result["date"] == ""
assert result["plain_text_body"] == ""
assert result["html_body"] == ""
def test_parse_multipart_email_missing_headers():
"""
Test parsing an email with missing headers in the payload.
"""
email_data = {
"id": "email111",
"threadId": "thread111",
"labelIds": ["INBOX"],
"historyId": "history111",
"snippet": "Missing headers email.",
"payload": {
# 'headers' key is missing
"body": {"size": 100, "data": "VGltZWw="}
},
}
with (
patch("arcade_google.utils._get_email_plain_text_body") as mock_plain,
patch("arcade_google.utils._get_email_html_body") as mock_html,
patch("arcade_google.utils._clean_email_body") as mock_clean,
):
# Mock the helper functions
mock_plain.return_value = "Timeel"
mock_html.return_value = "<p>Timeel</p>"
mock_clean.return_value = "Timeel"
result = parse_multipart_email(email_data)
assert result["id"] == "email111"
assert result["thread_id"] == "thread111"
assert result["label_ids"] == ["INBOX"]
assert result["snippet"] == "Missing headers email."
assert result["to"] == ""
assert result["from"] == ""
assert result["subject"] == ""
assert result["date"] == ""
assert result["plain_text_body"] == "Timeel"
assert result["html_body"] == "<p>Timeel</p>"
def test_parse_multipart_email_missing_fields():
"""
Test parsing an email with some missing fields in headers.
"""
email_data = {
"id": "email222",
"threadId": "thread222",
"labelIds": ["INBOX"],
"historyId": "history222",
"snippet": "Missing some headers.",
"payload": {
"headers": [
{"name": "From", "value": "sender4@example.com"},
{"name": "Subject", "value": "Partial Headers"},
# 'To' and 'Date' headers are missing
],
"body": {"size": 100, "data": "TWlzc2luZyBzb21lIGhlYWRlcnMu"},
},
}
with (
patch("arcade_google.utils._get_email_plain_text_body") as mock_plain,
patch("arcade_google.utils._get_email_html_body") as mock_html,
patch("arcade_google.utils._clean_email_body") as mock_clean,
):
# Mock the helper functions
mock_plain.return_value = "Missing some headers."
mock_html.return_value = None
mock_clean.return_value = "Missing some headers."
result = parse_multipart_email(email_data)
assert result["id"] == "email222"
assert result["thread_id"] == "thread222"
assert result["label_ids"] == ["INBOX"]
assert result["snippet"] == "Missing some headers."
assert result["to"] == ""
assert result["from"] == "sender4@example.com"
assert result["subject"] == "Partial Headers"
assert result["date"] == ""
assert result["plain_text_body"] == "Missing some headers."
assert result["html_body"] == ""
def test_parse_multipart_email_empty():
"""
Test parsing an empty email data.
"""
email_data = {}
result = parse_multipart_email(email_data)
assert result["id"] == ""
assert result["thread_id"] == ""
assert result["label_ids"] == []
assert result["snippet"] == ""
assert result["to"] == ""
assert result["from"] == ""
assert result["subject"] == ""
assert result["date"] == ""
assert result["plain_text_body"] == ""
assert result["html_body"] == ""
def test_parse_multipart_email_invalid_payload_structure():
"""
Test parsing an email with an invalid payload structure.
"""
email_data = {
"id": "email333",
"threadId": "thread333",
"labelIds": ["INBOX"],
"historyId": "history333",
"snippet": "Invalid payload structure.",
"payload": {
"headers": "This should be a list, not a string",
"body": {"size": 100, "data": "SW52YWxpZCBwYXlsb2Fk"},
},
}
with pytest.raises(TypeError):
parse_multipart_email(email_data)