use mcp_gmailcal::gmail_api::{EmailMessage, GmailService};
use mcp_gmailcal::config::Config;
use mcp_gmailcal::errors::GmailApiError;
use serde_json::{json, Value};
use base64::{encode, decode};
use std::time::{SystemTime, UNIX_EPOCH};
use mockito;
use std::env;
fn create_email_json(
id: &str,
thread_id: &str,
subject: &str,
from: &str,
to: &str,
body_text: &str,
body_html: Option<&str>,
) -> Value {
let headers = vec![
json!({"name": "Subject", "value": subject}),
json!({"name": "From", "value": from}),
json!({"name": "To", "value": to}),
json!({"name": "Date", "value": format!("Tue, 01 Apr 2025 12:34:56 +0000")}),
];
let mut parts = Vec::new();
let mut payload = json!({});
if let Some(html) = body_html {
parts.push(json!({
"mimeType": "text/plain",
"body": {
"data": encode_base64_url_safe(body_text),
"size": body_text.len()
}
}));
parts.push(json!({
"mimeType": "text/html",
"body": {
"data": encode_base64_url_safe(html),
"size": html.len()
}
}));
payload = json!({
"mimeType": "multipart/alternative",
"headers": headers,
"parts": parts
});
} else {
payload = json!({
"mimeType": "text/plain",
"headers": headers,
"body": {
"data": encode_base64_url_safe(body_text),
"size": body_text.len()
}
});
}
json!({
"id": id,
"threadId": thread_id,
"snippet": body_text.chars().take(50).collect::<String>(),
"payload": payload
})
}
fn encode_base64_url_safe(input: &str) -> String {
encode(input)
.replace('+', "-")
.replace('/', "_")
}
fn create_mock_config() -> Config {
Config {
client_id: "test_client_id".to_string(),
client_secret: "test_client_secret".to_string(),
refresh_token: "test_refresh_token".to_string(),
access_token: Some("test_access_token".to_string()),
token_refresh_threshold: 300, token_expiry_buffer: 60, }
}
#[ignore]
#[tokio::test]
async fn test_parse_simple_message() {
let mut server = mockito::Server::new();
let message_id = "12345";
let message_json = create_email_json(
message_id,
"thread123",
"Test Subject",
"sender@example.com",
"recipient@example.com",
"This is a test email body",
None, );
let mock = server.mock("GET", format!("/gmail/v1/users/me/messages/{}?format=full", message_id).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(message_json.to_string())
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
env::set_var("OAUTH_TOKEN_URL", format!("{}/token", server.url()));
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details(message_id).await;
mock.assert();
assert!(result.is_ok());
let message = result.unwrap();
assert_eq!(message.id, "12345");
assert_eq!(message.thread_id, "thread123");
assert_eq!(message.subject.unwrap(), "Test Subject");
assert_eq!(message.from.unwrap(), "sender@example.com");
assert_eq!(message.to.unwrap(), "recipient@example.com");
assert!(message.body_text.is_some());
assert_eq!(message.body_text.unwrap(), "This is a test email body");
assert!(message.body_html.is_none()); }
#[ignore]
#[tokio::test]
async fn test_parse_multipart_message() {
let mut server = mockito::Server::new();
let message_id = "67890";
let message_json = create_email_json(
message_id,
"thread456",
"Multipart Test",
"sender@example.com",
"recipient@example.com",
"This is the plain text version",
Some("<html><body>This is the HTML version</body></html>"), );
let mock = server.mock("GET", format!("/gmail/v1/users/me/messages/{}?format=full", message_id).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(message_json.to_string())
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details(message_id).await;
mock.assert();
assert!(result.is_ok());
let message = result.unwrap();
assert_eq!(message.id, "67890");
assert_eq!(message.thread_id, "thread456");
assert_eq!(message.subject.unwrap(), "Multipart Test");
assert_eq!(message.from.unwrap(), "sender@example.com");
assert_eq!(message.to.unwrap(), "recipient@example.com");
assert!(message.body_text.is_some());
assert!(message.body_html.is_some());
assert_eq!(message.body_text.unwrap(), "This is the plain text version");
assert_eq!(message.body_html.unwrap(), "<html><body>This is the HTML version</body></html>");
}
#[ignore]
#[tokio::test]
async fn test_parse_malformed_message() {
let mut server = mockito::Server::new();
let message_id = "malformed";
let malformed_json = json!({
"id": "malformed",
"snippet": "This is a malformed email",
"payload": {
"body": {
"data": encode_base64_url_safe("Malformed email body"),
"size": 19
},
"mimeType": "text/plain"
}
});
let mock = server.mock("GET", format!("/gmail/v1/users/me/messages/{}?format=full", message_id).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(malformed_json.to_string())
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details(message_id).await;
mock.assert();
assert!(result.is_err());
match result {
Err(GmailApiError::MessageFormatError(err)) => {
assert!(err.contains("threadId"), "Error should mention missing threadId field");
}
_ => panic!("Expected MessageFormatError"),
}
}
#[ignore]
#[tokio::test]
async fn test_list_messages() {
let mut server = mockito::Server::new();
let messages_json = json!({
"messages": [
{ "id": "msg1", "threadId": "thread1" },
{ "id": "msg2", "threadId": "thread2" },
{ "id": "msg3", "threadId": "thread3" }
],
"nextPageToken": "abc123",
"resultSizeEstimate": 3
});
let list_mock = server.mock("GET", "/gmail/v1/users/me/messages?maxResults=10")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(messages_json.to_string())
.create();
let msg1_json = create_email_json(
"msg1", "thread1", "First Email", "sender1@example.com",
"recipient@example.com", "First email body", None
);
let msg1_mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(msg1_json.to_string())
.create();
let msg2_json = create_email_json(
"msg2", "thread2", "Second Email", "sender2@example.com",
"recipient@example.com", "Second email body",
Some("<html><body>HTML content</body></html>")
);
let msg2_mock = server.mock("GET", "/gmail/v1/users/me/messages/msg2?format=full")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(msg2_json.to_string())
.create();
let msg3_json = create_email_json(
"msg3", "thread3", "Third Email", "sender3@example.com",
"recipient@example.com", "Third email body", None
);
let msg3_mock = server.mock("GET", "/gmail/v1/users/me/messages/msg3?format=full")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(msg3_json.to_string())
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.list_messages(10, None).await;
list_mock.assert();
msg1_mock.assert();
msg2_mock.assert();
msg3_mock.assert();
assert!(result.is_ok());
let messages = result.unwrap();
assert_eq!(messages.len(), 3);
assert_eq!(messages[0].id, "msg1");
assert_eq!(messages[0].subject.as_ref().unwrap(), "First Email");
assert_eq!(messages[1].id, "msg2");
assert_eq!(messages[1].subject.as_ref().unwrap(), "Second Email");
assert!(messages[1].body_html.is_some());
assert_eq!(messages[2].id, "msg3");
assert_eq!(messages[2].subject.as_ref().unwrap(), "Third Email");
}
#[ignore]
#[tokio::test]
async fn test_list_messages_with_query() {
let mut server = mockito::Server::new();
let messages_json = json!({
"messages": [
{ "id": "msg1", "threadId": "thread1" }
],
"resultSizeEstimate": 1
});
let list_mock = server.mock("GET", "/gmail/v1/users/me/messages?maxResults=10&q=important")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(messages_json.to_string())
.create();
let msg1_json = create_email_json(
"msg1", "thread1", "Important Email", "sender1@example.com",
"recipient@example.com", "This is an important email", None
);
let msg1_mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(msg1_json.to_string())
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.list_messages(10, Some("important")).await;
list_mock.assert();
msg1_mock.assert();
assert!(result.is_ok());
let messages = result.unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].id, "msg1");
assert_eq!(messages[0].subject.as_ref().unwrap(), "Important Email");
assert!(messages[0].body_text.as_ref().unwrap().contains("important"));
}
#[ignore]
#[tokio::test]
async fn test_error_handling_not_found() {
let mut server = mockito::Server::new();
let mock = server.mock("GET", "/gmail/v1/users/me/messages/nonexistent?format=full")
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"error":{"code":404,"message":"Not Found","status":"NOT_FOUND"}}"#)
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details("nonexistent").await;
mock.assert();
assert!(result.is_err());
match result {
Err(GmailApiError::MessageRetrievalError(err)) => {
assert!(err.contains("Not Found"), "Error should contain 'Not Found'");
}
_ => panic!("Expected MessageRetrievalError"),
}
}
#[ignore]
#[tokio::test]
async fn test_error_handling_unauthorized() {
let mut server = mockito::Server::new();
let mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
.with_status(401)
.with_header("content-type", "application/json")
.with_body(r#"{"error":{"code":401,"message":"Unauthorized","status":"UNAUTHENTICATED"}}"#)
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details("msg1").await;
mock.assert();
assert!(result.is_err());
match result {
Err(GmailApiError::AuthError(err)) => {
assert!(err.contains("Authentication failed"), "Error should indicate auth failure");
}
_ => panic!("Expected AuthError"),
}
}
#[ignore]
#[tokio::test]
async fn test_error_handling_rate_limit() {
let mut server = mockito::Server::new();
let mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
.with_status(429)
.with_header("content-type", "application/json")
.with_body(r#"{"error":{"code":429,"message":"Rate Limit Exceeded","status":"RESOURCE_EXHAUSTED"}}"#)
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details("msg1").await;
mock.assert();
assert!(result.is_err());
match result {
Err(GmailApiError::RateLimitError(err)) => {
assert!(err.contains("Rate limit exceeded"), "Error should indicate rate limiting");
}
_ => panic!("Expected RateLimitError"),
}
}
#[ignore]
#[tokio::test]
async fn test_error_handling_server_error() {
let mut server = mockito::Server::new();
let mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
.with_status(500)
.with_header("content-type", "application/json")
.with_body(r#"{"error":{"code":500,"message":"Internal Server Error","status":"INTERNAL"}}"#)
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details("msg1").await;
mock.assert();
assert!(result.is_err());
match result {
Err(GmailApiError::ApiError(err)) => {
assert!(err.contains("500"), "Error should contain status code");
assert!(err.contains("Internal Server Error"), "Error should contain error message");
}
_ => panic!("Expected ApiError"),
}
}
#[ignore]
#[tokio::test]
async fn test_error_handling_network_error() {
let server = mockito::Server::new();
let invalid_url = format!("{}/invalid_endpoint", server.url());
env::set_var("GMAIL_API_BASE_URL", invalid_url);
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details("msg1").await;
assert!(result.is_err());
match result {
Err(GmailApiError::NetworkError(err)) => {
assert!(err.contains("error") || err.contains("failed") || err.contains("connection"),
"Error should indicate network problem");
}
_ => panic!("Expected NetworkError, got: {:?}", result),
}
}
#[ignore]
#[tokio::test]
async fn test_special_characters_in_messages() {
let mut server = mockito::Server::new();
let message_id = "special";
let message_json = create_email_json(
message_id,
"thread_special",
"Special Characters: äöü 😊",
"sender@example.com",
"recipient@example.com",
"Email with emoji 🌍 and special chars: äöüß",
Some("<html><body>HTML with emoji 🎉 and special chars: äöüß</body></html>"),
);
let mock = server.mock("GET", format!("/gmail/v1/users/me/messages/{}?format=full", message_id).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(message_json.to_string())
.create();
env::set_var("GMAIL_API_BASE_URL", server.url());
let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
let result = gmail_service.get_message_details(message_id).await;
mock.assert();
assert!(result.is_ok());
let message = result.unwrap();
assert_eq!(message.id, "special");
assert!(message.subject.as_ref().unwrap().contains("äöü"));
assert!(message.subject.as_ref().unwrap().contains("😊"));
assert!(message.body_text.as_ref().unwrap().contains("🌍"));
assert!(message.body_text.as_ref().unwrap().contains("äöüß"));
assert!(message.body_html.as_ref().unwrap().contains("🎉"));
}