use chrono::{DateTime, Datelike, TimeZone, Timelike, Utc};
use mcp_gmailcal::gmail_api::{DraftEmail, EmailMessage};
use mcp_gmailcal::calendar_api::{Attendee, CalendarEvent};
use mcp_gmailcal::errors::GmailApiError;
use mcp_gmailcal::utils::{
decode_base64, encode_base64_url_safe, map_gmail_error, parse_max_results, to_mcp_error,
error_codes::{get_error_description, get_troubleshooting_steps},
error_codes::{AUTH_ERROR, API_ERROR, CONFIG_ERROR, MESSAGE_FORMAT_ERROR, GENERAL_ERROR}
};
use proptest::prelude::*;
use serde_json::{self, json, Value};
use std::collections::HashSet;
fn base64_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(any::<u8>(), 0..100)
.prop_map(|bytes| {
base64::encode(&bytes)
})
}
fn utf8_string_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-zA-Z0-9 !@#$%^&*()_+\\-=\\[\\]{};':\",./<>?\\\\|à éîøü]{1,100}")
.unwrap()
}
fn email_message_strategy() -> impl Strategy<Value = EmailMessage> {
(
"[a-zA-Z0-9_-]{6,22}".prop_map(String::from),
"[a-zA-Z0-9_-]{6,22}".prop_map(String::from),
proptest::option::weighted(0.9, utf8_string_strategy()),
proptest::option::weighted(0.9, "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}".prop_map(String::from)),
proptest::option::weighted(0.9, "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}".prop_map(String::from)),
proptest::option::weighted(0.9,
(1970i32..2030, 1u32..13, 1u32..28, 0u32..24, 0u32..60, 0u32..60)
.prop_map(|(year, month, day, hour, min, sec)| {
Utc.with_ymd_and_hms(year, month, day, hour, min, sec)
.unwrap()
.to_rfc3339()
})
),
proptest::option::weighted(0.9, utf8_string_strategy()),
proptest::option::weighted(0.8, utf8_string_strategy()),
proptest::option::weighted(0.7, utf8_string_strategy().prop_map(|s| format!("<div>{}</div>", s))),
).prop_map(|(id, thread_id, subject, from, to, date, snippet, body_text, body_html)| {
EmailMessage {
id,
thread_id,
subject,
from,
to,
date,
snippet,
body_text,
body_html,
}
})
}
fn draft_email_strategy() -> impl Strategy<Value = DraftEmail> {
(
"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}".prop_map(String::from),
utf8_string_strategy(),
utf8_string_strategy(),
proptest::option::weighted(0.7, "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}".prop_map(String::from)),
proptest::option::weighted(0.5, "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}".prop_map(String::from)),
proptest::option::weighted(0.5, "[a-zA-Z0-9_-]{6,22}".prop_map(String::from)),
proptest::option::weighted(0.4,
(
"[a-zA-Z0-9]{10,20}".prop_map(String::from),
"[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}".prop_map(String::from)
).prop_map(|(id, domain)| format!("<{}@{}>", id, domain))
),
proptest::option::weighted(0.3,
prop::collection::vec(
(
"[a-zA-Z0-9]{10,20}".prop_map(String::from),
"[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}".prop_map(String::from)
).prop_map(|(id, domain)| format!("<{}@{}>", id, domain)),
1..5
).prop_map(|refs| refs.join(" "))
),
).prop_map(|(to, subject, body, cc, bcc, thread_id, in_reply_to, references)| {
DraftEmail {
to,
subject,
body,
cc,
bcc,
thread_id,
in_reply_to,
references,
}
})
}
fn date_strategy() -> impl Strategy<Value = DateTime<Utc>> {
(1970i32..2030, 1u32..13, 1u32..28, 0u32..24, 0u32..60, 0u32..60)
.prop_map(|(year, month, day, hour, min, sec)| {
Utc.with_ymd_and_hms(year, month, day, hour, min, sec)
.unwrap()
})
}
fn calendar_event_strategy() -> impl Strategy<Value = CalendarEvent> {
(
proptest::option::weighted(0.8, "[a-zA-Z0-9_-]{6,22}".prop_map(String::from)),
utf8_string_strategy(),
proptest::option::weighted(0.7, utf8_string_strategy()),
proptest::option::weighted(0.7, utf8_string_strategy()),
date_strategy(),
prop::collection::vec(
(
"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}".prop_map(String::from),
proptest::option::weighted(0.7, utf8_string_strategy()),
proptest::option::weighted(0.7, proptest::string::string_regex("(needsAction|declined|tentative|accepted)").unwrap()),
proptest::option::weighted(0.3, prop::bool::ANY)
).prop_map(|(email, display_name, response_status, optional)| {
Attendee {
email,
display_name,
response_status,
optional,
}
}),
0..5
),
).prop_flat_map(|(id, summary, description, location, start_time, attendees)| {
(Just(id), Just(summary), Just(description), Just(location),
Just(start_time),
(1i64..100).prop_map(move |hours_to_add| start_time + chrono::Duration::hours(hours_to_add)),
Just(attendees))
}).prop_map(|(id, summary, description, location, start_time, end_time, attendees)| {
CalendarEvent {
id,
summary,
description,
location,
start_time,
end_time,
attendees,
conference_data: None, html_link: None,
creator: None,
organizer: None,
}
})
}
fn max_results_values_strategy() -> impl Strategy<Value = Value> {
prop_oneof![
(1u32..100).prop_map(|n| json!(n)),
(1u32..100).prop_map(|n| json!(n.to_string())),
Just(json!(null)),
Just(json!(true)),
Just(json!(false)),
Just(json!([])),
Just(json!({}))
]
}
proptest! {
#[test]
fn test_base64_roundtrip(data: Vec<u8>) {
let encoded = encode_base64_url_safe(&data);
let decoded = match base64::decode_config(&encoded, base64::URL_SAFE) {
Ok(d) => d,
Err(e) => {
prop_assert!(
false,
"Failed to decode base64 string '{}': {}",
encoded, e
);
return Ok(());
}
};
prop_assert_eq!(&data, &decoded);
}
#[test]
fn test_decode_base64_with_valid_input(s in "A{0,20}") {
let valid_base64 = base64::encode(s.as_bytes());
let result = decode_base64(&valid_base64);
prop_assert!(result.is_ok());
}
#[test]
fn test_parse_max_results_property(value in max_results_values_strategy(), default in 1u32..100) {
let result = parse_max_results(Some(value.clone()), default);
match value {
Value::Number(n) if n.is_u64() && n.as_u64().unwrap() <= u32::MAX as u64 => {
prop_assert_eq!(result, n.as_u64().unwrap() as u32);
},
Value::String(s) if s.parse::<u32>().is_ok() => {
prop_assert_eq!(result, s.parse::<u32>().unwrap());
},
_ => prop_assert_eq!(result, default),
}
}
#[test]
fn test_email_message_serialization(email in email_message_strategy()) {
let json = serde_json::to_string(&email).unwrap();
let deserialized: EmailMessage = serde_json::from_str(&json).unwrap();
prop_assert_eq!(email.id, deserialized.id);
prop_assert_eq!(email.thread_id, deserialized.thread_id);
prop_assert_eq!(email.subject, deserialized.subject);
prop_assert_eq!(email.from, deserialized.from);
prop_assert_eq!(email.to, deserialized.to);
prop_assert_eq!(email.date, deserialized.date);
prop_assert_eq!(email.snippet, deserialized.snippet);
prop_assert_eq!(email.body_text, deserialized.body_text);
prop_assert_eq!(email.body_html, deserialized.body_html);
}
#[test]
fn test_draft_email_serialization(draft in draft_email_strategy()) {
let json = serde_json::to_string(&draft).unwrap();
let deserialized: DraftEmail = serde_json::from_str(&json).unwrap();
prop_assert_eq!(draft.to, deserialized.to);
prop_assert_eq!(draft.subject, deserialized.subject);
prop_assert_eq!(draft.body, deserialized.body);
prop_assert_eq!(draft.cc, deserialized.cc);
prop_assert_eq!(draft.bcc, deserialized.bcc);
prop_assert_eq!(draft.thread_id, deserialized.thread_id);
prop_assert_eq!(draft.in_reply_to, deserialized.in_reply_to);
prop_assert_eq!(draft.references, deserialized.references);
}
#[test]
fn test_datetime_serialization(dt in date_strategy()) {
let date_str = dt.to_rfc3339();
let parsed = DateTime::parse_from_rfc3339(&date_str).unwrap().with_timezone(&Utc);
prop_assert_eq!(dt, parsed);
}
#[test]
fn test_calendar_event_serialization(event in calendar_event_strategy()) {
let json = serde_json::to_string(&event).unwrap();
let deserialized: CalendarEvent = serde_json::from_str(&json).unwrap();
prop_assert_eq!(event.id, deserialized.id);
prop_assert_eq!(event.summary, deserialized.summary);
prop_assert_eq!(event.description, deserialized.description);
prop_assert_eq!(event.location, deserialized.location);
prop_assert_eq!(event.start_time, deserialized.start_time);
prop_assert_eq!(event.end_time, deserialized.end_time);
prop_assert_eq!(event.attendees.len(), deserialized.attendees.len());
for (original, deserialized_attendee) in event.attendees.iter().zip(deserialized.attendees.iter()) {
prop_assert_eq!(&original.email, &deserialized_attendee.email);
prop_assert_eq!(&original.display_name, &deserialized_attendee.display_name);
prop_assert_eq!(&original.response_status, &deserialized_attendee.response_status);
prop_assert_eq!(&original.optional, &deserialized_attendee.optional);
}
}
#[test]
fn test_calendar_event_invariants(event in calendar_event_strategy()) {
prop_assert!(event.end_time > event.start_time,
"End time {:?} should be after start time {:?}", event.end_time, event.start_time);
let mut emails = HashSet::new();
for attendee in &event.attendees {
prop_assert!(emails.insert(attendee.email.clone()),
"Duplicate attendee email: {}", attendee.email);
}
}
}
macro_rules! test_json_value_parsing {
($name:ident, $value:expr, $expected:expr, $default:expr) => {
#[test]
fn $name() {
let value = $value;
let result = parse_max_results(Some(value), $default);
assert_eq!(result, $expected);
}
};
}
test_json_value_parsing!(test_parse_max_results_with_number, json!(10), 10, 5);
test_json_value_parsing!(test_parse_max_results_with_string, json!("20"), 20, 5);
test_json_value_parsing!(test_parse_max_results_with_null, json!(null), 5, 5);
test_json_value_parsing!(test_parse_max_results_with_boolean, json!(true), 5, 5);
test_json_value_parsing!(test_parse_max_results_with_array, json!([1, 2, 3]), 5, 5);
test_json_value_parsing!(test_parse_max_results_with_object, json!({"value": 30}), 5, 5);
test_json_value_parsing!(test_parse_max_results_with_invalid_string, json!("not a number"), 5, 5);
test_json_value_parsing!(test_parse_max_results_with_float, json!(15.7), 5, 5); test_json_value_parsing!(test_parse_max_results_with_large_number, json!(4_294_967_295_i64 + 1), 5, 5); test_json_value_parsing!(test_parse_max_results_with_negative, json!(-5), 5, 5);
#[test]
fn test_base64_encoding_special_cases() {
let empty: Vec<u8> = vec![];
let encoded = encode_base64_url_safe(&empty);
assert_eq!(encoded, "");
let result = decode_base64("");
assert_eq!(result.unwrap(), "");
let special_chars = "Hello+World/Special_Chars".as_bytes().to_vec();
let encoded = encode_base64_url_safe(&special_chars);
assert!(!encoded.contains('+'));
assert!(!encoded.contains('/'));
let decoded = decode_base64(&encoded).unwrap();
assert_eq!(decoded.as_bytes(), special_chars);
let invalid = "abc";
let result = decode_base64(invalid);
assert!(result.is_err());
let invalid = "a$c=";
let result = decode_base64(invalid);
assert!(result.is_err());
}
#[test]
fn test_email_message_invariants() {
let email = EmailMessage {
id: "msg123".to_string(),
thread_id: "thread456".to_string(),
subject: Some("Test Email".to_string()),
from: Some("sender@example.com".to_string()),
to: Some("recipient@example.com".to_string()),
date: Some("2023-05-15T10:00:00Z".to_string()),
snippet: Some("This is a test email...".to_string()),
body_text: Some("This is the plain text body.".to_string()),
body_html: Some("<div>This is the HTML body.</div>".to_string()),
};
let json = serde_json::to_string(&email).unwrap();
let deserialized: EmailMessage = serde_json::from_str(&json).unwrap();
assert!(!deserialized.id.is_empty());
assert!(!deserialized.thread_id.is_empty());
assert_eq!(email.subject, deserialized.subject);
assert_eq!(email.from, deserialized.from);
assert_eq!(email.to, deserialized.to);
assert_eq!(email.date, deserialized.date);
assert_eq!(email.snippet, deserialized.snippet);
assert_eq!(email.body_text, deserialized.body_text);
assert_eq!(email.body_html, deserialized.body_html);
}
#[test]
fn test_datetime_parsing_edge_cases() {
let formats = [
"2023-05-15T10:00:00Z",
"2023-05-15T10:00:00+00:00",
"2023-05-15T10:00:00.123Z",
"2023-05-15T10:00:00.123456Z",
];
for format in formats {
let parsed = DateTime::parse_from_rfc3339(format).unwrap();
let utc = parsed.with_timezone(&Utc);
assert_eq!(utc.year(), 2023);
assert_eq!(utc.month(), 5);
assert_eq!(utc.day(), 15);
assert_eq!(utc.hour(), 10);
assert_eq!(utc.minute(), 0);
assert_eq!(utc.second(), 0);
}
let with_timezone = "2023-05-15T10:00:00+05:00"; let parsed = DateTime::parse_from_rfc3339(with_timezone).unwrap();
let utc = parsed.with_timezone(&Utc);
assert_eq!(utc.hour(), 5);
}
fn error_message_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-zA-Z0-9 ]{0,50}").unwrap()
}
proptest! {
#[test]
fn test_error_descriptions_for_all_codes(code in 0u32..10000u32) {
let desc = get_error_description(code);
prop_assert!(!desc.is_empty());
let steps = get_troubleshooting_steps(code);
prop_assert!(!steps.is_empty());
}
#[test]
fn test_to_mcp_error_with_arbitrary_inputs(message in error_message_strategy(), code in 1000u32..1010u32) {
let error = to_mcp_error(&message, code);
let debug_str = format!("{:?}", error);
prop_assert!(debug_str.contains(&code.to_string()));
}
#[test]
fn test_map_gmail_error_with_arbitrary_inputs(message in error_message_strategy()) {
let api_error = map_gmail_error(GmailApiError::ApiError(message.clone()));
let debug_str = format!("{:?}", api_error);
let auth_error = map_gmail_error(GmailApiError::AuthError(message.clone()));
let debug_str2 = format!("{:?}", auth_error);
let retrieval_error = map_gmail_error(GmailApiError::MessageRetrievalError(message.clone()));
let debug_str3 = format!("{:?}", retrieval_error);
let format_error = map_gmail_error(GmailApiError::MessageFormatError(message.clone()));
let debug_str4 = format!("{:?}", format_error);
let network_error = map_gmail_error(GmailApiError::NetworkError(message.clone()));
let debug_str5 = format!("{:?}", network_error);
let rate_limit_error = map_gmail_error(GmailApiError::RateLimitError(message.clone()));
let debug_str6 = format!("{:?}", rate_limit_error);
prop_assert!(debug_str.contains(&API_ERROR.to_string())
|| debug_str.contains(&AUTH_ERROR.to_string())
|| debug_str.contains(&MESSAGE_FORMAT_ERROR.to_string()));
prop_assert!(debug_str2.contains(&AUTH_ERROR.to_string()));
prop_assert!(debug_str3.contains(&API_ERROR.to_string()));
prop_assert!(debug_str4.contains(&MESSAGE_FORMAT_ERROR.to_string()));
prop_assert!(debug_str5.contains(&API_ERROR.to_string()));
prop_assert!(debug_str6.contains(&API_ERROR.to_string()));
}
#[test]
fn test_api_error_message_classification(message in error_message_strategy()) {
let error1 = map_gmail_error(GmailApiError::ApiError(format!("quota {}", message)));
let debug_str1 = format!("{:?}", error1);
let error2 = map_gmail_error(GmailApiError::ApiError(format!("rate {}", message)));
let debug_str2 = format!("{:?}", error2);
let error3 = map_gmail_error(GmailApiError::ApiError(format!("limit {}", message)));
let debug_str3 = format!("{:?}", error3);
let error4 = map_gmail_error(GmailApiError::ApiError(format!("network {}", message)));
let debug_str4 = format!("{:?}", error4);
let error5 = map_gmail_error(GmailApiError::ApiError(format!("connection {}", message)));
let debug_str5 = format!("{:?}", error5);
let error6 = map_gmail_error(GmailApiError::ApiError(format!("timeout {}", message)));
let debug_str6 = format!("{:?}", error6);
let error7 = map_gmail_error(GmailApiError::ApiError(format!("authentication {}", message)));
let debug_str7 = format!("{:?}", error7);
let error8 = map_gmail_error(GmailApiError::ApiError(format!("auth {}", message)));
let debug_str8 = format!("{:?}", error8);
let error9 = map_gmail_error(GmailApiError::ApiError(format!("token {}", message)));
let debug_str9 = format!("{:?}", error9);
let error10 = map_gmail_error(GmailApiError::ApiError(format!("format {}", message)));
let debug_str10 = format!("{:?}", error10);
let error11 = map_gmail_error(GmailApiError::ApiError(format!("missing field {}", message)));
let debug_str11 = format!("{:?}", error11);
let error12 = map_gmail_error(GmailApiError::ApiError(format!("parse {}", message)));
let debug_str12 = format!("{:?}", error12);
let error13 = map_gmail_error(GmailApiError::ApiError(format!("not found {}", message)));
let debug_str13 = format!("{:?}", error13);
let error14 = map_gmail_error(GmailApiError::ApiError(format!("404 {}", message)));
let debug_str14 = format!("{:?}", error14);
prop_assert!(debug_str1.contains(&API_ERROR.to_string()));
prop_assert!(debug_str2.contains(&API_ERROR.to_string()));
prop_assert!(debug_str3.contains(&API_ERROR.to_string()));
prop_assert!(debug_str4.contains(&API_ERROR.to_string()));
prop_assert!(debug_str5.contains(&API_ERROR.to_string()));
prop_assert!(debug_str6.contains(&API_ERROR.to_string()));
prop_assert!(debug_str7.contains(&AUTH_ERROR.to_string()));
prop_assert!(debug_str8.contains(&AUTH_ERROR.to_string()));
prop_assert!(debug_str9.contains(&AUTH_ERROR.to_string()));
prop_assert!(debug_str10.contains(&MESSAGE_FORMAT_ERROR.to_string()));
prop_assert!(debug_str11.contains(&MESSAGE_FORMAT_ERROR.to_string()));
prop_assert!(debug_str12.contains(&MESSAGE_FORMAT_ERROR.to_string()));
prop_assert!(debug_str13.contains(&API_ERROR.to_string()));
prop_assert!(debug_str14.contains(&API_ERROR.to_string()));
}
}