use mcp_gmailcal::{
config::Config,
errors::GmailApiError,
prompts,
GmailServer,
};
use serde_json::json;
use std::env;
use std::sync::Once;
use chrono::{Datelike, Timelike, Utc};
static INIT: Once = Once::new();
fn setup() {
INIT.call_once(|| {
env::set_var("GMAIL_CLIENT_ID", "test_client_id");
env::set_var("GMAIL_CLIENT_SECRET", "test_client_secret");
env::set_var("GMAIL_REFRESH_TOKEN", "test_refresh_token");
env::set_var("GMAIL_ACCESS_TOKEN", "test_access_token");
env::set_var("GMAIL_REDIRECT_URI", "test_redirect_uri");
});
}
#[test]
fn test_gmail_prompt() {
assert!(!prompts::GMAIL_MASTER_PROMPT.is_empty());
assert!(prompts::GMAIL_MASTER_PROMPT.contains("Gmail Assistant"));
assert!(!prompts::EMAIL_ANALYSIS_PROMPT.is_empty());
assert!(prompts::EMAIL_ANALYSIS_PROMPT.contains("analyzing emails"));
assert!(!prompts::EMAIL_SUMMARIZATION_PROMPT.is_empty());
assert!(prompts::EMAIL_SUMMARIZATION_PROMPT.contains("summarizing emails"));
assert!(!prompts::EMAIL_SEARCH_PROMPT.is_empty());
assert!(prompts::EMAIL_SEARCH_PROMPT.contains("search for emails"));
assert!(!prompts::TASK_EXTRACTION_PROMPT.is_empty());
assert!(prompts::TASK_EXTRACTION_PROMPT.contains("extracting tasks"));
assert!(!prompts::MEETING_EXTRACTION_PROMPT.is_empty());
assert!(prompts::MEETING_EXTRACTION_PROMPT.contains("extracting meeting"));
assert!(!prompts::CONTACT_EXTRACTION_PROMPT.is_empty());
assert!(prompts::CONTACT_EXTRACTION_PROMPT.contains("extracting contact"));
assert!(!prompts::EMAIL_CATEGORIZATION_PROMPT.is_empty());
assert!(prompts::EMAIL_CATEGORIZATION_PROMPT.contains("categorizing emails"));
assert!(!prompts::EMAIL_PRIORITIZATION_PROMPT.is_empty());
assert!(prompts::EMAIL_PRIORITIZATION_PROMPT.contains("prioritize emails"));
assert!(!prompts::EMAIL_DRAFTING_PROMPT.is_empty());
assert!(prompts::EMAIL_DRAFTING_PROMPT.contains("draft effective"));
}
#[test]
fn test_server_creation() {
setup();
let _server = GmailServer::new();
env::set_var("GMAIL_CLIENT_ID", "test_client_id");
env::set_var("GMAIL_CLIENT_SECRET", "test_client_secret");
env::set_var("GMAIL_REFRESH_TOKEN", "test_refresh_token");
env::set_var("GMAIL_ACCESS_TOKEN", "test_access_token");
let 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, };
assert_eq!(config.client_id, "test_client_id");
assert_eq!(config.client_secret, "test_client_secret");
assert_eq!(config.refresh_token, "test_refresh_token");
assert_eq!(config.access_token.unwrap(), "test_access_token");
}
#[test]
fn test_configuration() {
setup();
let original_client_id = env::var("GMAIL_CLIENT_ID").unwrap_or_default();
struct EnvGuard {
key: &'static str,
value: String,
}
impl Drop for EnvGuard {
fn drop(&mut self) {
env::set_var(self.key, &self.value);
}
}
let _guard = EnvGuard {
key: "GMAIL_CLIENT_ID",
value: original_client_id.clone(),
};
env::set_var("GMAIL_CLIENT_ID", "test_client_id");
env::set_var("GMAIL_CLIENT_SECRET", "test_client_secret");
env::set_var("GMAIL_REFRESH_TOKEN", "test_refresh_token");
env::set_var("GMAIL_ACCESS_TOKEN", "test_access_token");
env::set_var("GMAIL_REDIRECT_URI", "test_redirect_uri");
assert_eq!(env::var("GMAIL_CLIENT_ID").unwrap(), "test_client_id");
assert_eq!(
env::var("GMAIL_CLIENT_SECRET").unwrap(),
"test_client_secret"
);
assert_eq!(
env::var("GMAIL_REFRESH_TOKEN").unwrap(),
"test_refresh_token"
);
assert_eq!(env::var("GMAIL_ACCESS_TOKEN").unwrap(), "test_access_token");
assert_eq!(env::var("GMAIL_REDIRECT_URI").unwrap(), "test_redirect_uri");
env::set_var("DOTENV_PATH", "/tmp/nonexistent_dotenv_file_for_tests");
let valid_config = Config::from_env();
assert!(valid_config.is_ok(), "Config should be valid with all variables set");
env::remove_var("GMAIL_CLIENT_ID");
assert!(env::var("GMAIL_CLIENT_ID").is_err());
let invalid_config = Config::from_env();
assert!(invalid_config.is_err(), "Config should fail with missing GMAIL_CLIENT_ID");
env::set_var("GMAIL_CLIENT_ID", "test_client_id");
}
#[test]
fn test_command_handling() {
setup();
let json_command = r#"
{
"command": "list_messages",
"params": {
"max_results": 5,
"query": "important"
}
}
"#;
let parsed: serde_json::Value = serde_json::from_str(json_command).unwrap();
assert_eq!(parsed["command"], "list_messages");
assert_eq!(parsed["params"]["max_results"], 5);
assert_eq!(parsed["params"]["query"], "important");
let invalid_json = r#"
{
"command": "list_messages",
"params": {
"max_results": 5,
"query": "important"
}
"#;
let parse_result = serde_json::from_str::<serde_json::Value>(invalid_json);
assert!(parse_result.is_err());
}
#[test]
fn test_email_workflows() {
setup();
let email_json = json!({
"id": "msg123456",
"threadId": "thread123456",
"historyId": "12345",
"snippet": "This is an email about a meeting next week",
"labelIds": ["INBOX", "UNREAD"],
"date": "2023-05-15T10:00:00Z",
"from": "sender@example.com",
"to": "recipient@example.com",
"subject": "Meeting Next Week",
"body": "Let's meet next week to discuss the project progress."
});
let search_results = json!({
"messages": [
{
"id": "msg123456",
"threadId": "thread123456"
},
{
"id": "msg789012",
"threadId": "thread789012"
}
],
"nextPageToken": "token123",
"resultSizeEstimate": 2
});
assert_eq!(email_json["id"], "msg123456");
assert_eq!(email_json["from"], "sender@example.com");
assert_eq!(email_json["subject"], "Meeting Next Week");
assert_eq!(search_results["messages"].as_array().unwrap().len(), 2);
assert_eq!(search_results["messages"][0]["id"], "msg123456");
assert_eq!(search_results["resultSizeEstimate"], 2);
let email_content = email_json["body"].as_str().unwrap();
assert!(email_content.contains("meet next week"));
let has_meeting_intent = email_content.contains("meet") &&
(email_json["subject"].as_str().unwrap().contains("Meeting") ||
email_content.contains("meeting"));
assert!(has_meeting_intent, "Email should contain meeting intent");
}
#[test]
fn test_calendar_operations() {
setup();
let now = Utc::now();
let tomorrow = now + chrono::Duration::days(1);
let end_time = tomorrow + chrono::Duration::hours(1);
let event_data = json!({
"summary": "Project Status Meeting",
"description": "Discuss current project status and next steps",
"location": "Conference Room B",
"start": {
"dateTime": tomorrow.to_rfc3339(),
"timeZone": "UTC"
},
"end": {
"dateTime": end_time.to_rfc3339(),
"timeZone": "UTC"
},
"attendees": [
{
"email": "team@example.com"
},
{
"email": "manager@example.com"
}
]
});
let calendar_list = json!({
"items": [
{
"id": "primary",
"summary": "Primary Calendar",
"primary": true
},
{
"id": "calendar123",
"summary": "Work Calendar"
}
]
});
assert_eq!(event_data["summary"], "Project Status Meeting");
assert!(event_data["start"]["dateTime"].as_str().unwrap().len() > 0);
assert_eq!(event_data["attendees"].as_array().unwrap().len(), 2);
assert_eq!(calendar_list["items"].as_array().unwrap().len(), 2);
assert_eq!(calendar_list["items"][0]["id"], "primary");
assert_eq!(calendar_list["items"][0]["summary"], "Primary Calendar");
let start_date = event_data["start"]["dateTime"].as_str().unwrap();
let end_date = event_data["end"]["dateTime"].as_str().unwrap();
assert!(start_date < end_date, "End date should be after start date");
assert_eq!(event_data["start"]["timeZone"], "UTC");
assert_eq!(event_data["end"]["timeZone"], "UTC");
}
#[test]
fn test_contact_operations() {
setup();
let contacts_list = json!({
"connections": [
{
"resourceName": "people/c12345",
"names": [
{
"displayName": "John Doe",
"familyName": "Doe",
"givenName": "John"
}
],
"emailAddresses": [
{
"value": "john.doe@example.com",
"type": "work"
}
],
"phoneNumbers": [
{
"value": "+1234567890",
"type": "mobile"
}
]
},
{
"resourceName": "people/c67890",
"names": [
{
"displayName": "Jane Smith",
"familyName": "Smith",
"givenName": "Jane"
}
],
"emailAddresses": [
{
"value": "jane.smith@example.com",
"type": "work"
}
]
}
]
});
assert_eq!(contacts_list["connections"].as_array().unwrap().len(), 2);
assert_eq!(
contacts_list["connections"][0]["names"][0]["displayName"],
"John Doe"
);
assert_eq!(
contacts_list["connections"][0]["emailAddresses"][0]["value"],
"john.doe@example.com"
);
let email_to_find = "john.doe@example.com";
let mut found_contact = None;
for contact in contacts_list["connections"].as_array().unwrap() {
if let Some(emails) = contact["emailAddresses"].as_array() {
for email in emails {
if email["value"] == email_to_find {
found_contact = Some(contact);
break;
}
}
}
if found_contact.is_some() {
break;
}
}
assert!(found_contact.is_some(), "Should find contact by email");
assert_eq!(
found_contact.unwrap()["names"][0]["displayName"],
"John Doe"
);
}
#[test]
fn test_authentication_flows() {
setup();
let _auth_code = "4/P7q7W91a-oMsCeLvIaQm6bTrgtp7";
let token_response = json!({
"access_token": "ya29.a0ARrdaM-8ESz8QsScH1YW7pyQyVkT_hxLBp",
"expires_in": 3599,
"refresh_token": "1//0eHB-yGB0O5MuCgYIARAAGA4SNwF",
"scope": "https://www.googleapis.com/auth/gmail.readonly",
"token_type": "Bearer"
});
assert!(token_response["access_token"].as_str().unwrap().len() > 0);
assert!(token_response["refresh_token"].as_str().unwrap().len() > 0);
assert_eq!(token_response["token_type"], "Bearer");
let 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, };
assert_eq!(config.refresh_token, "test_refresh_token");
assert_eq!(config.access_token.unwrap(), "test_access_token");
}
#[test]
fn test_error_recovery() {
setup();
struct RetryState {
attempts: u32,
max_attempts: u32,
}
impl RetryState {
fn new(max_attempts: u32) -> Self {
Self {
attempts: 0,
max_attempts,
}
}
fn attempt<T>(&mut self, success_value: T, error_fn: impl Fn() -> GmailApiError) -> Result<T, GmailApiError> {
self.attempts += 1;
if self.attempts < self.max_attempts {
Err(error_fn())
} else {
Ok(success_value)
}
}
}
let mut rate_limit_state = RetryState::new(3);
let result = rate_limit_state.attempt(
"Success",
|| GmailApiError::RateLimitError("Rate limit exceeded".to_string())
);
assert!(result.is_err());
match result {
Err(GmailApiError::RateLimitError(msg)) => {
assert_eq!(msg, "Rate limit exceeded");
},
_ => panic!("Expected RateLimitError")
}
let result = rate_limit_state.attempt(
"Success",
|| GmailApiError::RateLimitError("Rate limit exceeded".to_string())
);
assert!(result.is_err());
let result = rate_limit_state.attempt(
"Success",
|| GmailApiError::RateLimitError("Rate limit exceeded".to_string())
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Success");
let mut network_error_state = RetryState::new(2);
let result = network_error_state.attempt(
"Connected",
|| GmailApiError::NetworkError("Connection failed".to_string())
);
assert!(result.is_err());
let result = network_error_state.attempt(
"Connected",
|| GmailApiError::NetworkError("Connection failed".to_string())
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Connected");
}
#[test]
fn test_cross_api_workflows() {
setup();
let email = json!({
"id": "email123",
"threadId": "thread123",
"subject": "Project Meeting",
"from": "colleague@example.com",
"snippet": "Let's meet to discuss the project next Monday at 2 PM",
"body": "Hi team,\n\nLet's schedule a meeting to discuss our project progress.\n\nDate: Next Monday\nTime: 2:00 PM - 3:00 PM\nLocation: Conference Room A\n\nPlease confirm if you can attend.\n\nRegards,\nYour Colleague"
});
struct MeetingDetails {
title: String,
date: String,
time: String,
location: String,
attendees: Vec<String>,
}
let meeting = MeetingDetails {
title: "Project Meeting".to_string(),
date: "Next Monday".to_string(),
time: "2:00 PM - 3:00 PM".to_string(),
location: "Conference Room A".to_string(),
attendees: vec!["colleague@example.com".to_string(), "team@example.com".to_string()],
};
assert_eq!(email["subject"], meeting.title);
assert!(email["body"].as_str().unwrap().contains(&meeting.date));
assert!(email["body"].as_str().unwrap().contains(&meeting.time));
assert!(email["body"].as_str().unwrap().contains(&meeting.location));
let contacts = json!({
"connections": [
{
"resourceName": "people/c12345",
"names": [
{
"displayName": "Colleague",
"givenName": "Colleague"
}
],
"emailAddresses": [
{
"value": "colleague@example.com"
}
]
}
]
});
assert_eq!(contacts["connections"][0]["emailAddresses"][0]["value"], meeting.attendees[0]);
let now = Utc::now();
let next_monday = now + chrono::Duration::days(((8 - now.weekday().num_days_from_monday() as i64) % 7) + 1);
let event_start = next_monday.with_hour(14).unwrap().with_minute(0).unwrap().with_second(0).unwrap();
let event_end = event_start + chrono::Duration::hours(1);
let event = json!({
"summary": meeting.title,
"location": meeting.location,
"description": "Meeting scheduled based on email conversation.",
"start": {
"dateTime": event_start.to_rfc3339(),
"timeZone": "UTC"
},
"end": {
"dateTime": event_end.to_rfc3339(),
"timeZone": "UTC"
},
"attendees": [
{
"email": meeting.attendees[0]
},
{
"email": meeting.attendees[1]
}
]
});
assert_eq!(event["summary"], meeting.title);
assert_eq!(event["location"], meeting.location);
assert_eq!(event["attendees"].as_array().unwrap().len(), 2);
assert_eq!(event["attendees"][0]["email"], meeting.attendees[0]);
let draft = json!({
"id": "draft123",
"threadId": email["threadId"],
"to": email["from"],
"subject": format!("Re: {}", email["subject"].as_str().unwrap()),
"body": "I've added the meeting to my calendar and sent invites to all attendees. Looking forward to it!"
});
assert_eq!(draft["threadId"], email["threadId"]);
assert_eq!(draft["to"], email["from"]);
assert!(draft["subject"].as_str().unwrap().starts_with("Re: "));
assert!(draft["body"].as_str().unwrap().contains("added the meeting"));
}