use chrono::{DateTime, Duration, TimeZone, Utc};
use mockall::mock;
use mcp_gmailcal::calendar_api::{Attendee, CalendarEvent, CalendarInfo, CalendarList, ConferenceData, ConferenceSolution, EntryPoint, EventOrganizer};
use mcp_gmailcal::errors::CalendarApiError;
use reqwest::Client;
use std::sync::Arc;
use std::sync::Mutex;
use uuid::Uuid;
trait CalendarClientInterface {
fn list_calendars(&self) -> Result<CalendarList, CalendarApiError>;
fn list_events(
&self,
calendar_id: &str,
max_results: Option<u32>,
time_min: Option<DateTime<Utc>>,
time_max: Option<DateTime<Utc>>,
) -> Result<Vec<CalendarEvent>, CalendarApiError>;
fn create_event(
&self,
calendar_id: &str,
event: CalendarEvent,
) -> Result<CalendarEvent, CalendarApiError>;
fn get_event(
&self,
calendar_id: &str,
event_id: &str,
) -> Result<CalendarEvent, CalendarApiError>;
}
struct MockableCalendarClient {
client: Arc<Mutex<dyn CalendarClientInterface + Send + Sync>>,
}
impl MockableCalendarClient {
fn new(client: Arc<Mutex<dyn CalendarClientInterface + Send + Sync>>) -> Self {
Self { client }
}
fn list_calendars(&self) -> Result<CalendarList, CalendarApiError> {
let client = self.client.lock().unwrap();
client.list_calendars()
}
fn list_events(
&self,
calendar_id: &str,
max_results: Option<u32>,
time_min: Option<DateTime<Utc>>,
time_max: Option<DateTime<Utc>>,
) -> Result<Vec<CalendarEvent>, CalendarApiError> {
let client = self.client.lock().unwrap();
client.list_events(calendar_id, max_results, time_min, time_max)
}
fn create_event(
&self,
calendar_id: &str,
event: CalendarEvent,
) -> Result<CalendarEvent, CalendarApiError> {
let client = self.client.lock().unwrap();
client.create_event(calendar_id, event)
}
fn get_event(
&self,
calendar_id: &str,
event_id: &str,
) -> Result<CalendarEvent, CalendarApiError> {
let client = self.client.lock().unwrap();
client.get_event(calendar_id, event_id)
}
}
struct MockCalendarClient {
calendars: Vec<CalendarInfo>,
events: Vec<CalendarEvent>,
should_fail: bool,
}
impl MockCalendarClient {
fn new() -> Self {
let calendars = vec![
CalendarInfo {
id: "primary".to_string(),
summary: "Primary Calendar".to_string(),
description: Some("Your main calendar".to_string()),
primary: Some(true),
},
CalendarInfo {
id: "work@example.com".to_string(),
summary: "Work Calendar".to_string(),
description: Some("Calendar for work events".to_string()),
primary: Some(false),
},
CalendarInfo {
id: "family@example.com".to_string(),
summary: "Family Calendar".to_string(),
description: Some("Calendar for family events".to_string()),
primary: Some(false),
},
];
let events = vec![
create_test_event("event1", "Meeting with team", "Office", Utc::now() + Duration::hours(1), Utc::now() + Duration::hours(2)),
create_test_event("event2", "Lunch with client", "Restaurant", Utc::now() + Duration::hours(5), Utc::now() + Duration::hours(6)),
create_test_event("event3", "Project deadline", "Office", Utc::now() + Duration::days(2), Utc::now() + Duration::days(2) + Duration::hours(8)),
];
Self {
calendars,
events,
should_fail: false,
}
}
fn with_failure(mut self) -> Self {
self.should_fail = true;
self
}
}
impl CalendarClientInterface for MockCalendarClient {
fn list_calendars(&self) -> Result<CalendarList, CalendarApiError> {
if self.should_fail {
return Err(CalendarApiError::ApiError("Failed to list calendars".to_string()));
}
Ok(CalendarList {
calendars: self.calendars.clone(),
next_page_token: None,
})
}
fn list_events(
&self,
calendar_id: &str,
max_results: Option<u32>,
time_min: Option<DateTime<Utc>>,
time_max: Option<DateTime<Utc>>,
) -> Result<Vec<CalendarEvent>, CalendarApiError> {
if self.should_fail {
return Err(CalendarApiError::ApiError("Failed to list events".to_string()));
}
if calendar_id.is_empty() {
return Err(CalendarApiError::ApiError("Calendar ID cannot be empty".to_string()));
}
let filtered_events = self.events.iter()
.filter(|event| {
let time_min_check = match time_min {
Some(min_time) => event.start_time >= min_time,
None => true,
};
let time_max_check = match time_max {
Some(max_time) => event.start_time <= max_time,
None => true,
};
time_min_check && time_max_check
})
.cloned()
.collect::<Vec<CalendarEvent>>();
if let Some(max) = max_results {
Ok(filtered_events.into_iter().take(max as usize).collect())
} else {
Ok(filtered_events)
}
}
fn create_event(
&self,
calendar_id: &str,
mut event: CalendarEvent,
) -> Result<CalendarEvent, CalendarApiError> {
if self.should_fail {
return Err(CalendarApiError::ApiError("Failed to create event".to_string()));
}
if calendar_id.is_empty() {
return Err(CalendarApiError::ApiError("Calendar ID cannot be empty".to_string()));
}
if event.summary.is_empty() {
return Err(CalendarApiError::EventFormatError("Event summary cannot be empty".to_string()));
}
if event.end_time <= event.start_time {
return Err(CalendarApiError::EventFormatError("End time must be after start time".to_string()));
}
if event.id.is_none() {
event.id = Some(Uuid::new_v4().to_string());
}
if event.html_link.is_none() {
event.html_link = Some(format!(
"https://calendar.google.com/calendar/event?eid={}",
event.id.as_ref().unwrap()
));
}
Ok(event)
}
fn get_event(
&self,
calendar_id: &str,
event_id: &str,
) -> Result<CalendarEvent, CalendarApiError> {
if self.should_fail {
return Err(CalendarApiError::ApiError("Failed to get event".to_string()));
}
if calendar_id.is_empty() {
return Err(CalendarApiError::ApiError("Calendar ID cannot be empty".to_string()));
}
if event_id.is_empty() {
return Err(CalendarApiError::ApiError("Event ID cannot be empty".to_string()));
}
let event = self.events.iter()
.find(|e| e.id.as_ref().map_or(false, |id| id == event_id))
.cloned();
match event {
Some(event) => Ok(event),
None => Err(CalendarApiError::EventRetrievalError(format!("Event {} not found", event_id))),
}
}
}
fn create_test_event(
id: &str,
summary: &str,
location: &str,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
) -> CalendarEvent {
CalendarEvent {
id: Some(id.to_string()),
summary: summary.to_string(),
description: Some(format!("Description for {}", summary)),
location: Some(location.to_string()),
creator: Some(EventOrganizer {
email: "creator@example.com".to_string(),
display_name: Some("Event Creator".to_string()),
self_: Some(true),
}),
organizer: Some(EventOrganizer {
email: "organizer@example.com".to_string(),
display_name: Some("Event Organizer".to_string()),
self_: Some(false),
}),
start_time,
end_time,
attendees: vec![
Attendee {
email: "attendee1@example.com".to_string(),
display_name: Some("Attendee 1".to_string()),
response_status: Some("accepted".to_string()),
optional: None,
},
Attendee {
email: "attendee2@example.com".to_string(),
display_name: Some("Attendee 2".to_string()),
response_status: Some("tentative".to_string()),
optional: None,
},
],
html_link: Some(format!(
"https://calendar.google.com/calendar/event?eid={}",
id
)),
conference_data: Some(ConferenceData {
conference_solution: Some(ConferenceSolution {
name: "Google Meet".to_string(),
key: Some("meet".to_string()),
}),
entry_points: vec![
EntryPoint {
entry_point_type: "video".to_string(),
uri: "https://meet.google.com/abc-defg-hij".to_string(),
label: Some("Google Meet".to_string()),
},
EntryPoint {
entry_point_type: "phone".to_string(),
uri: "tel:+11234567890".to_string(),
label: Some("Phone".to_string()),
},
],
}),
}
}
fn create_test_calendar(id: &str, summary: &str, is_primary: bool) -> CalendarInfo {
CalendarInfo {
id: id.to_string(),
summary: summary.to_string(),
description: Some(format!("Description for {}", summary)),
primary: Some(is_primary),
}
}
#[cfg(test)]
mod comprehensive_calendar_tests {
use super::*;
fn create_test_client() -> MockableCalendarClient {
let mock_client = MockCalendarClient::new();
let client = Arc::new(Mutex::new(mock_client));
MockableCalendarClient::new(client)
}
fn create_failing_client() -> MockableCalendarClient {
let mock_client = MockCalendarClient::new().with_failure();
let client = Arc::new(Mutex::new(mock_client));
MockableCalendarClient::new(client)
}
#[test]
fn test_list_calendars_success() {
let client = create_test_client();
let result = client.list_calendars();
assert!(result.is_ok());
let calendars = result.unwrap();
assert_eq!(calendars.calendars.len(), 3);
let primary = calendars.calendars.iter().find(|c| c.primary == Some(true)).unwrap();
assert_eq!(primary.id, "primary");
assert_eq!(primary.summary, "Primary Calendar");
let work_calendar = calendars.calendars.iter().find(|c| c.id == "work@example.com").unwrap();
assert_eq!(work_calendar.summary, "Work Calendar");
let family_calendar = calendars.calendars.iter().find(|c| c.id == "family@example.com").unwrap();
assert_eq!(family_calendar.summary, "Family Calendar");
}
#[test]
fn test_list_calendars_failure() {
let client = create_failing_client();
let result = client.list_calendars();
assert!(result.is_err());
match result {
Err(CalendarApiError::ApiError(msg)) => {
assert_eq!(msg, "Failed to list calendars");
}
_ => panic!("Expected ApiError"),
}
}
#[test]
fn test_list_events_success() {
let client = create_test_client();
let result = client.list_events("primary", None, None, None);
assert!(result.is_ok());
let events = result.unwrap();
assert_eq!(events.len(), 3);
let result = client.list_events("primary", Some(2), None, None);
assert!(result.is_ok());
let events = result.unwrap();
assert_eq!(events.len(), 2);
let time_min = Utc::now();
let result = client.list_events("primary", None, Some(time_min), None);
assert!(result.is_ok());
let events = result.unwrap();
assert!(events.len() > 0);
for event in events {
assert!(event.start_time >= time_min);
}
}
#[test]
fn test_list_events_failure() {
let client = create_failing_client();
let result = client.list_events("primary", None, None, None);
assert!(result.is_err());
match result {
Err(CalendarApiError::ApiError(msg)) => {
assert_eq!(msg, "Failed to list events");
}
_ => panic!("Expected ApiError"),
}
}
#[test]
fn test_create_event_success() {
let client = create_test_client();
let new_event = CalendarEvent {
id: None, summary: "New Test Event".to_string(),
description: Some("Description for new test event".to_string()),
location: Some("Test Location".to_string()),
start_time: Utc::now() + Duration::hours(24),
end_time: Utc::now() + Duration::hours(25),
attendees: vec![
Attendee {
email: "test@example.com".to_string(),
display_name: Some("Test User".to_string()),
response_status: None,
optional: None,
},
],
html_link: None, conference_data: None,
creator: None,
organizer: None,
};
let result = client.create_event("primary", new_event.clone());
assert!(result.is_ok());
let created_event = result.unwrap();
assert!(created_event.id.is_some());
assert!(created_event.html_link.is_some());
assert_eq!(created_event.summary, new_event.summary);
assert_eq!(created_event.description, new_event.description);
assert_eq!(created_event.location, new_event.location);
assert_eq!(created_event.start_time, new_event.start_time);
assert_eq!(created_event.end_time, new_event.end_time);
}
#[test]
fn test_create_event_validation_failure() {
let client = create_test_client();
let invalid_event = CalendarEvent {
id: None,
summary: "".to_string(), description: Some("Description".to_string()),
location: Some("Location".to_string()),
start_time: Utc::now() + Duration::hours(1),
end_time: Utc::now() + Duration::hours(2),
attendees: vec![],
html_link: None,
conference_data: None,
creator: None,
organizer: None,
};
let result = client.create_event("primary", invalid_event);
assert!(result.is_err());
match result {
Err(CalendarApiError::EventFormatError(msg)) => {
assert!(msg.contains("summary cannot be empty"));
}
_ => panic!("Expected EventFormatError"),
}
let invalid_event = CalendarEvent {
id: None,
summary: "Invalid Time Event".to_string(),
description: Some("Description".to_string()),
location: Some("Location".to_string()),
start_time: Utc::now() + Duration::hours(2), end_time: Utc::now() + Duration::hours(1), attendees: vec![],
html_link: None,
conference_data: None,
creator: None,
organizer: None,
};
let result = client.create_event("primary", invalid_event);
assert!(result.is_err());
match result {
Err(CalendarApiError::EventFormatError(msg)) => {
assert!(msg.contains("End time must be after start time"));
}
_ => panic!("Expected EventFormatError"),
}
let valid_event = CalendarEvent {
id: None,
summary: "Valid Event".to_string(),
description: Some("Description".to_string()),
location: Some("Location".to_string()),
start_time: Utc::now() + Duration::hours(1),
end_time: Utc::now() + Duration::hours(2),
attendees: vec![],
html_link: None,
conference_data: None,
creator: None,
organizer: None,
};
let result = client.create_event("", valid_event);
assert!(result.is_err());
match result {
Err(CalendarApiError::ApiError(msg)) => {
assert!(msg.contains("Calendar ID cannot be empty"));
}
_ => panic!("Expected ApiError"),
}
}
#[test]
fn test_create_event_failure() {
let client = create_failing_client();
let event = CalendarEvent {
id: None,
summary: "Test Event".to_string(),
description: Some("Description".to_string()),
location: Some("Location".to_string()),
start_time: Utc::now() + Duration::hours(1),
end_time: Utc::now() + Duration::hours(2),
attendees: vec![],
html_link: None,
conference_data: None,
creator: None,
organizer: None,
};
let result = client.create_event("primary", event);
assert!(result.is_err());
match result {
Err(CalendarApiError::ApiError(msg)) => {
assert_eq!(msg, "Failed to create event");
}
_ => panic!("Expected ApiError"),
}
}
#[test]
fn test_get_event_success() {
let client = create_test_client();
let result = client.get_event("primary", "event1");
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.id, Some("event1".to_string()));
assert_eq!(event.summary, "Meeting with team");
assert_eq!(event.location, Some("Office".to_string()));
assert!(event.conference_data.is_some());
let conference_data = event.conference_data.unwrap();
assert!(conference_data.conference_solution.is_some());
assert_eq!(conference_data.conference_solution.unwrap().name, "Google Meet");
assert_eq!(conference_data.entry_points.len(), 2);
assert_eq!(event.attendees.len(), 2);
assert_eq!(event.attendees[0].email, "attendee1@example.com");
assert_eq!(event.attendees[1].email, "attendee2@example.com");
}
#[test]
fn test_get_event_not_found() {
let client = create_test_client();
let result = client.get_event("primary", "nonexistent");
assert!(result.is_err());
match result {
Err(CalendarApiError::EventRetrievalError(msg)) => {
assert!(msg.contains("Event nonexistent not found"));
}
_ => panic!("Expected EventRetrievalError"),
}
}
#[test]
fn test_get_event_invalid_params() {
let client = create_test_client();
let result = client.get_event("", "event1");
assert!(result.is_err());
match result {
Err(CalendarApiError::ApiError(msg)) => {
assert!(msg.contains("Calendar ID cannot be empty"));
}
_ => panic!("Expected ApiError"),
}
let result = client.get_event("primary", "");
assert!(result.is_err());
match result {
Err(CalendarApiError::ApiError(msg)) => {
assert!(msg.contains("Event ID cannot be empty"));
}
_ => panic!("Expected ApiError"),
}
}
#[test]
fn test_get_event_failure() {
let client = create_failing_client();
let result = client.get_event("primary", "event1");
assert!(result.is_err());
match result {
Err(CalendarApiError::ApiError(msg)) => {
assert_eq!(msg, "Failed to get event");
}
_ => panic!("Expected ApiError"),
}
}
#[test]
fn test_timezone_handling() {
let utc_time = Utc.ymd(2025, 5, 15).and_hms(10, 0, 0);
let berlin_time = DateTime::parse_from_rfc3339("2025-05-15T12:00:00+02:00").unwrap();
let berlin_in_utc = berlin_time.with_timezone(&Utc);
let pt_time = DateTime::parse_from_rfc3339("2025-05-15T03:00:00-07:00").unwrap();
let pt_in_utc = pt_time.with_timezone(&Utc);
assert_eq!(utc_time.to_rfc3339(), "2025-05-15T10:00:00+00:00");
assert_eq!(berlin_in_utc.to_rfc3339(), "2025-05-15T10:00:00+00:00");
assert_eq!(pt_in_utc.to_rfc3339(), "2025-05-15T10:00:00+00:00");
}
#[test]
fn test_all_day_event_handling() {
let start_of_day = Utc.ymd(2025, 5, 15).and_hms(0, 0, 0);
let end_of_day = Utc.ymd(2025, 5, 15).and_hms(23, 59, 59);
assert_eq!(start_of_day.to_rfc3339(), "2025-05-15T00:00:00+00:00");
assert_eq!(end_of_day.to_rfc3339(), "2025-05-15T23:59:59+00:00");
let date_str = "2025-05-15";
assert_eq!(date_str, "2025-05-15");
}
#[test]
fn test_recurring_event_parameters() {
let client = create_test_client();
let event = CalendarEvent {
id: None,
summary: "Recurring Test Event".to_string(),
description: Some("This would be a recurring event".to_string()),
location: Some("Test Location".to_string()),
start_time: Utc::now() + Duration::hours(1),
end_time: Utc::now() + Duration::hours(2),
attendees: vec![],
html_link: None,
conference_data: None,
creator: None,
organizer: None,
};
let result = client.create_event("primary", event);
assert!(result.is_ok());
}
#[test]
fn test_conference_data_handling() {
let client = create_test_client();
let result = client.get_event("primary", "event1");
assert!(result.is_ok());
let event = result.unwrap();
assert!(event.conference_data.is_some());
let conf_data = event.conference_data.unwrap();
assert!(conf_data.conference_solution.is_some());
let solution = conf_data.conference_solution.unwrap();
assert_eq!(solution.name, "Google Meet");
assert_eq!(solution.key, Some("meet".to_string()));
assert_eq!(conf_data.entry_points.len(), 2);
let video_entry = conf_data.entry_points.iter()
.find(|ep| ep.entry_point_type == "video")
.unwrap();
assert!(video_entry.uri.contains("meet.google.com"));
let phone_entry = conf_data.entry_points.iter()
.find(|ep| ep.entry_point_type == "phone")
.unwrap();
assert!(phone_entry.uri.contains("tel:"));
}
}