From 545ede43255056451fd7c3ed0d5065e5a426ae1d Mon Sep 17 00:00:00 2001 From: Jason Scurtu <590307+xarbit@users.noreply.github.com> Date: Mon, 1 Dec 2025 02:09:10 +0000 Subject: [PATCH 1/2] Improve error handling and add comprehensive unit tests - Remove redundant eprintln! calls from main.rs (log::error! already handles logging) - Add comprehensive unit tests for EventHandler (17 new tests): - Event validation edge cases (whitespace, unicode, long titles) - EventError display and debug formatting - Add comprehensive unit tests for CalendarHandler (12 new tests): - Validation edge cases (empty color, whitespace-only name, unicode) - CalendarError display and debug formatting - Default color validation - Add comprehensive unit tests for ExportHandler (13 new tests): - iCal export with various fields (location, notes, url, unicode) - Minimal event export - ExportError display and debug formatting --- src/main.rs | 2 - src/services/calendar_handler.rs | 76 ++++++++++++++++++++ src/services/event_handler.rs | 114 +++++++++++++++++++++++++++++ src/services/export_handler.rs | 118 +++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1cf7e58..2a864ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,13 +53,11 @@ pub fn main() -> cosmic::iced::Result { } Err(e) => { log::error!("Failed to clear events: {}", e); - eprintln!("Failed to clear events: {}", e); } } } Err(e) => { log::error!("Failed to open database: {}", e); - eprintln!("Failed to open database: {}", e); } } } diff --git a/src/services/calendar_handler.rs b/src/services/calendar_handler.rs index 8f96563..4d02c8c 100644 --- a/src/services/calendar_handler.rs +++ b/src/services/calendar_handler.rs @@ -266,6 +266,8 @@ impl CalendarHandler { mod tests { use super::*; + // ==================== Validation Tests ==================== + #[test] fn test_validate_empty_name() { let data = NewCalendarData { @@ -276,6 +278,26 @@ mod tests { assert!(matches!(result, Err(CalendarError::ValidationError(_)))); } + #[test] + fn test_validate_whitespace_only_name() { + let data = NewCalendarData { + name: " ".to_string(), + color: "#FF0000".to_string(), + }; + let result = CalendarHandler::validate(&data); + assert!(matches!(result, Err(CalendarError::ValidationError(_)))); + } + + #[test] + fn test_validate_empty_color() { + let data = NewCalendarData { + name: "Work".to_string(), + color: "".to_string(), + }; + let result = CalendarHandler::validate(&data); + assert!(matches!(result, Err(CalendarError::ValidationError(_)))); + } + #[test] fn test_validate_valid_data() { let data = NewCalendarData { @@ -286,10 +308,64 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_validate_unicode_name() { + let data = NewCalendarData { + name: "工作日历 📅".to_string(), + color: "#3B82F6".to_string(), + }; + let result = CalendarHandler::validate(&data); + assert!(result.is_ok()); + } + + // ==================== Default Color Tests ==================== + #[test] fn test_default_color() { let color = CalendarHandler::default_color(); assert!(!color.is_empty()); assert!(color.starts_with('#')); } + + #[test] + fn test_default_color_is_valid_hex() { + let color = CalendarHandler::default_color(); + assert!(color.len() == 7); // #RRGGBB format + assert!(color.chars().skip(1).all(|c| c.is_ascii_hexdigit())); + } + + // ==================== CalendarError Display Tests ==================== + + #[test] + fn test_calendar_error_display_not_found() { + let error = CalendarError::NotFound("cal-123".to_string()); + assert_eq!(error.to_string(), "Calendar not found: cal-123"); + } + + #[test] + fn test_calendar_error_display_validation() { + let error = CalendarError::ValidationError("Name required".to_string()); + assert_eq!(error.to_string(), "Invalid calendar: Name required"); + } + + #[test] + fn test_calendar_error_display_config() { + let error = CalendarError::ConfigError("Write failed".to_string()); + assert_eq!(error.to_string(), "Config error: Write failed"); + } + + #[test] + fn test_calendar_error_display_duplicate_id() { + let error = CalendarError::DuplicateId("work".to_string()); + assert_eq!(error.to_string(), "Calendar ID already exists: work"); + } + + // ==================== CalendarError Debug Tests ==================== + + #[test] + fn test_calendar_error_debug() { + let error = CalendarError::NotFound("test".to_string()); + let debug_str = format!("{:?}", error); + assert!(debug_str.contains("NotFound")); + } } diff --git a/src/services/event_handler.rs b/src/services/event_handler.rs index f34207a..b22e932 100644 --- a/src/services/event_handler.rs +++ b/src/services/event_handler.rs @@ -317,6 +317,8 @@ mod tests { } } + // ==================== Event Validation Tests ==================== + #[test] fn test_validate_event_success() { let event = create_test_event("test-1", "Valid Event"); @@ -330,6 +332,13 @@ mod tests { assert!(matches!(result, Err(EventError::ValidationError(_)))); } + #[test] + fn test_validate_event_whitespace_only_title() { + let event = create_test_event("test-1", " "); + let result = EventHandler::validate_event(&event); + assert!(matches!(result, Err(EventError::ValidationError(_)))); + } + #[test] fn test_validate_event_empty_uid() { let mut event = create_test_event("", "Test Event"); @@ -345,4 +354,109 @@ mod tests { let result = EventHandler::validate_event(&event); assert!(matches!(result, Err(EventError::ValidationError(_)))); } + + #[test] + fn test_validate_event_same_start_and_end() { + let mut event = create_test_event("test-1", "Zero Duration Event"); + event.end = event.start; // Same time - should be valid (zero duration) + let result = EventHandler::validate_event(&event); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_event_with_all_fields() { + let mut event = create_test_event("test-full", "Complete Event"); + event.location = Some("Test Location".to_string()); + event.all_day = true; + event.notes = Some("Test notes".to_string()); + event.url = Some("https://example.com".to_string()); + event.invitees = vec!["test@example.com".to_string()]; + event.travel_time = TravelTime::ThirtyMinutes; + event.repeat = RepeatFrequency::Weekly; + event.alert = AlertTime::FifteenMinutes; + event.alert_second = Some(AlertTime::OneHour); + event.attachments = vec!["file.pdf".to_string()]; + + let result = EventHandler::validate_event(&event); + assert!(result.is_ok()); + } + + // ==================== EventError Display Tests ==================== + + #[test] + fn test_event_error_display_validation() { + let error = EventError::ValidationError("Test validation error".to_string()); + assert_eq!(error.to_string(), "Validation error: Test validation error"); + } + + #[test] + fn test_event_error_display_calendar_not_found() { + let error = EventError::CalendarNotFound("cal-123".to_string()); + assert_eq!(error.to_string(), "Calendar not found: cal-123"); + } + + #[test] + fn test_event_error_display_event_not_found() { + let error = EventError::EventNotFound("event-456".to_string()); + assert_eq!(error.to_string(), "Event not found: event-456"); + } + + #[test] + fn test_event_error_display_storage() { + let error = EventError::StorageError("Database connection failed".to_string()); + assert_eq!(error.to_string(), "Storage error: Database connection failed"); + } + + #[test] + fn test_event_error_display_sync() { + let error = EventError::SyncError("Network timeout".to_string()); + assert_eq!(error.to_string(), "Sync error: Network timeout"); + } + + // ==================== EventError Debug Tests ==================== + + #[test] + fn test_event_error_debug() { + let error = EventError::ValidationError("test".to_string()); + let debug_str = format!("{:?}", error); + assert!(debug_str.contains("ValidationError")); + } + + // ==================== Edge Case Tests ==================== + + #[test] + fn test_validate_event_unicode_title() { + let event = create_test_event("test-unicode", "会议 📅 Meeting"); + let result = EventHandler::validate_event(&event); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_event_very_long_title() { + let long_title = "A".repeat(1000); + let event = create_test_event("test-long", &long_title); + let result = EventHandler::validate_event(&event); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_event_special_chars_in_uid() { + let event = create_test_event("test-uid-with-special@chars.com", "Special UID Event"); + let result = EventHandler::validate_event(&event); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_event_multiline_title() { + let event = create_test_event("test-multiline", "Line 1\nLine 2"); + let result = EventHandler::validate_event(&event); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_event_tab_only_title() { + let event = create_test_event("test-tabs", "\t\t"); + let result = EventHandler::validate_event(&event); + assert!(matches!(result, Err(EventError::ValidationError(_)))); + } } diff --git a/src/services/export_handler.rs b/src/services/export_handler.rs index b4e465c..88b6cad 100644 --- a/src/services/export_handler.rs +++ b/src/services/export_handler.rs @@ -238,6 +238,8 @@ mod tests { } } + // ==================== Event to iCal Tests ==================== + #[test] fn test_event_to_ical() { let event = create_test_event(); @@ -250,4 +252,120 @@ mod tests { assert!(ical_string.contains("END:VEVENT")); assert!(ical_string.contains("END:VCALENDAR")); } + + #[test] + fn test_event_to_ical_with_location() { + let event = create_test_event(); + let ical = ExportHandler::event_to_ical(&event); + let ical_string = ical.to_string(); + + assert!(ical_string.contains("LOCATION:Test Location")); + } + + #[test] + fn test_event_to_ical_with_notes() { + let event = create_test_event(); + let ical = ExportHandler::event_to_ical(&event); + let ical_string = ical.to_string(); + + assert!(ical_string.contains("DESCRIPTION:Test notes")); + } + + #[test] + fn test_event_to_ical_with_uid() { + let event = create_test_event(); + let ical = ExportHandler::event_to_ical(&event); + let ical_string = ical.to_string(); + + assert!(ical_string.contains("UID:test-export-1")); + } + + #[test] + fn test_event_to_ical_minimal_event() { + let event = CalendarEvent { + uid: "minimal-1".to_string(), + summary: "Minimal Event".to_string(), + location: None, + all_day: false, + start: Utc.with_ymd_and_hms(2025, 12, 1, 10, 0, 0).unwrap(), + end: Utc.with_ymd_and_hms(2025, 12, 1, 11, 0, 0).unwrap(), + travel_time: TravelTime::None, + repeat: RepeatFrequency::Never, + invitees: vec![], + alert: AlertTime::None, + alert_second: None, + attachments: vec![], + url: None, + notes: None, + }; + + let ical = ExportHandler::event_to_ical(&event); + let ical_string = ical.to_string(); + + assert!(ical_string.contains("BEGIN:VCALENDAR")); + assert!(ical_string.contains("Minimal Event")); + assert!(!ical_string.contains("LOCATION:")); + assert!(!ical_string.contains("DESCRIPTION:")); + } + + #[test] + fn test_event_to_ical_with_url() { + let mut event = create_test_event(); + event.url = Some("https://example.com/meeting".to_string()); + + let ical = ExportHandler::event_to_ical(&event); + let ical_string = ical.to_string(); + + assert!(ical_string.contains("URL:https://example.com/meeting")); + } + + #[test] + fn test_event_to_ical_unicode_content() { + let mut event = create_test_event(); + event.summary = "会议 Meeting 📅".to_string(); + event.location = Some("北京 Beijing".to_string()); + event.notes = Some("备注 Notes 🗒️".to_string()); + + let ical = ExportHandler::event_to_ical(&event); + let ical_string = ical.to_string(); + + assert!(ical_string.contains("会议 Meeting 📅")); + assert!(ical_string.contains("北京 Beijing")); + assert!(ical_string.contains("备注 Notes 🗒️")); + } + + // ==================== ExportError Display Tests ==================== + + #[test] + fn test_export_error_display_io() { + let error = ExportError::IoError("File not found".to_string()); + assert_eq!(error.to_string(), "I/O error: File not found"); + } + + #[test] + fn test_export_error_display_format() { + let error = ExportError::FormatError("Invalid iCal format".to_string()); + assert_eq!(error.to_string(), "Format error: Invalid iCal format"); + } + + #[test] + fn test_export_error_display_parse() { + let error = ExportError::ParseError("Unexpected token".to_string()); + assert_eq!(error.to_string(), "Parse error: Unexpected token"); + } + + #[test] + fn test_export_error_display_calendar_not_found() { + let error = ExportError::CalendarNotFound("personal".to_string()); + assert_eq!(error.to_string(), "Calendar not found: personal"); + } + + // ==================== ExportError Debug Tests ==================== + + #[test] + fn test_export_error_debug() { + let error = ExportError::IoError("test".to_string()); + let debug_str = format!("{:?}", error); + assert!(debug_str.contains("IoError")); + } } From 2d1ade2e6147b6b661da33f80354e5625b8e6405 Mon Sep 17 00:00:00 2001 From: Jason Scurtu <590307+xarbit@users.noreply.github.com> Date: Mon, 1 Dec 2025 02:19:00 +0000 Subject: [PATCH 2/2] Add comprehensive unit test coverage across core modules - validation.rs: 15 tests covering date parsing, title validation, email validation, and date range functions with edge cases - cache.rs: 27 tests covering cache creation, month navigation, precaching, cleanup, and month arithmetic - database/schema.rs: 14 tests covering CRUD operations, multiple calendars, unicode support, and edge cases - protocols/local.rs: 12 tests covering Protocol trait, event operations, and multiple calendar isolation - locale.rs: 31 tests covering 24-hour detection, first day of week, date formats, week range formatting, and day headers Total new tests: ~100 across 5 modules --- src/cache.rs | 269 +++++++++++++++++++++++++++++++ src/database/schema.rs | 292 ++++++++++++++++++++++++++++++++++ src/locale.rs | 351 ++++++++++++++++++++++++++++++++++++++++- src/protocols/local.rs | 164 +++++++++++++++++++ src/validation.rs | 118 +++++++++++++- 5 files changed, 1182 insertions(+), 12 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 6c5629c..bf30b91 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -147,3 +147,272 @@ pub struct CacheStats { pub period_texts_cached: usize, pub current_month: (i32, u32), } + +#[cfg(test)] +mod tests { + use super::*; + + // ==================== Cache Creation Tests ==================== + + #[test] + fn test_cache_new() { + let cache = CalendarCache::new(2024, 6); + assert_eq!(cache.current, (2024, 6)); + assert!(cache.states.contains_key(&(2024, 6))); + assert!(cache.period_texts.contains_key(&(2024, 6))); + } + + #[test] + fn test_cache_new_january() { + let cache = CalendarCache::new(2024, 1); + assert_eq!(cache.current, (2024, 1)); + } + + #[test] + fn test_cache_new_december() { + let cache = CalendarCache::new(2024, 12); + assert_eq!(cache.current, (2024, 12)); + } + + // ==================== set_current Tests ==================== + + #[test] + fn test_set_current_new_month() { + let mut cache = CalendarCache::new(2024, 6); + cache.set_current(2024, 7); + assert_eq!(cache.current, (2024, 7)); + assert!(cache.states.contains_key(&(2024, 7))); + } + + #[test] + fn test_set_current_same_month() { + let mut cache = CalendarCache::new(2024, 6); + let initial_states_count = cache.states.len(); + cache.set_current(2024, 6); + assert_eq!(cache.states.len(), initial_states_count); + } + + #[test] + fn test_set_current_cross_year_forward() { + let mut cache = CalendarCache::new(2024, 12); + cache.set_current(2025, 1); + assert_eq!(cache.current, (2025, 1)); + assert!(cache.states.contains_key(&(2025, 1))); + } + + #[test] + fn test_set_current_cross_year_backward() { + let mut cache = CalendarCache::new(2024, 1); + cache.set_current(2023, 12); + assert_eq!(cache.current, (2023, 12)); + assert!(cache.states.contains_key(&(2023, 12))); + } + + // ==================== current_state Tests ==================== + + #[test] + fn test_current_state() { + let cache = CalendarCache::new(2024, 6); + let state = cache.current_state(); + assert_eq!(state.year, 2024); + assert_eq!(state.month, 6); + } + + // ==================== current_period_text Tests ==================== + + #[test] + fn test_current_period_text() { + let cache = CalendarCache::new(2024, 6); + let text = cache.current_period_text(); + assert!(text.contains("June")); + assert!(text.contains("2024")); + } + + #[test] + fn test_current_period_text_january() { + let cache = CalendarCache::new(2024, 1); + let text = cache.current_period_text(); + assert!(text.contains("January")); + } + + #[test] + fn test_current_period_text_december() { + let cache = CalendarCache::new(2024, 12); + let text = cache.current_period_text(); + assert!(text.contains("December")); + } + + // ==================== current_month_text Tests ==================== + + #[test] + fn test_current_month_text() { + let cache = CalendarCache::new(2024, 6); + assert_eq!(cache.current_month_text(), "June"); + } + + #[test] + fn test_current_month_text_all_months() { + let months = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + for (i, month_name) in months.iter().enumerate() { + let cache = CalendarCache::new(2024, (i + 1) as u32); + assert_eq!(cache.current_month_text(), *month_name); + } + } + + // ==================== current_year_text Tests ==================== + + #[test] + fn test_current_year_text() { + let cache = CalendarCache::new(2024, 6); + assert_eq!(cache.current_year_text(), "2024"); + } + + #[test] + fn test_current_year_text_various_years() { + for year in [1999, 2000, 2024, 2050, 2100] { + let cache = CalendarCache::new(year, 1); + assert_eq!(cache.current_year_text(), year.to_string()); + } + } + + // ==================== add_months Tests ==================== + + #[test] + fn test_add_months_same_year() { + let cache = CalendarCache::new(2024, 1); + assert_eq!(cache.add_months(2024, 1, 1), (2024, 2)); + assert_eq!(cache.add_months(2024, 1, 6), (2024, 7)); + assert_eq!(cache.add_months(2024, 1, 11), (2024, 12)); + } + + #[test] + fn test_add_months_cross_year() { + let cache = CalendarCache::new(2024, 1); + assert_eq!(cache.add_months(2024, 1, 12), (2025, 1)); + assert_eq!(cache.add_months(2024, 6, 12), (2025, 6)); + assert_eq!(cache.add_months(2024, 12, 1), (2025, 1)); + } + + #[test] + fn test_add_months_multiple_years() { + let cache = CalendarCache::new(2024, 1); + assert_eq!(cache.add_months(2024, 1, 24), (2026, 1)); + assert_eq!(cache.add_months(2024, 1, 36), (2027, 1)); + } + + // ==================== subtract_months Tests ==================== + + #[test] + fn test_subtract_months_same_year() { + let cache = CalendarCache::new(2024, 12); + assert_eq!(cache.subtract_months(2024, 12, 1), (2024, 11)); + assert_eq!(cache.subtract_months(2024, 12, 6), (2024, 6)); + assert_eq!(cache.subtract_months(2024, 12, 11), (2024, 1)); + } + + #[test] + fn test_subtract_months_cross_year() { + let cache = CalendarCache::new(2024, 1); + assert_eq!(cache.subtract_months(2024, 1, 1), (2023, 12)); + assert_eq!(cache.subtract_months(2024, 6, 12), (2023, 6)); + } + + #[test] + fn test_subtract_months_multiple_years() { + let cache = CalendarCache::new(2024, 1); + assert_eq!(cache.subtract_months(2024, 1, 24), (2022, 1)); + assert_eq!(cache.subtract_months(2024, 1, 36), (2021, 1)); + } + + // ==================== precache_surrounding Tests ==================== + + #[test] + fn test_precache_surrounding() { + let mut cache = CalendarCache::new(2024, 6); + cache.precache_surrounding(2, 2); + + // Should have current + 2 before + 2 after = 5 months cached + assert!(cache.states.contains_key(&(2024, 4))); + assert!(cache.states.contains_key(&(2024, 5))); + assert!(cache.states.contains_key(&(2024, 6))); + assert!(cache.states.contains_key(&(2024, 7))); + assert!(cache.states.contains_key(&(2024, 8))); + } + + #[test] + fn test_precache_surrounding_cross_year() { + let mut cache = CalendarCache::new(2024, 1); + cache.precache_surrounding(2, 2); + + // Should cache Nov 2023, Dec 2023, Jan 2024, Feb 2024, Mar 2024 + assert!(cache.states.contains_key(&(2023, 11))); + assert!(cache.states.contains_key(&(2023, 12))); + assert!(cache.states.contains_key(&(2024, 1))); + assert!(cache.states.contains_key(&(2024, 2))); + assert!(cache.states.contains_key(&(2024, 3))); + } + + // ==================== cleanup Tests ==================== + + #[test] + fn test_cleanup() { + let mut cache = CalendarCache::new(2024, 6); + // Pre-cache many months + cache.precache_surrounding(6, 6); + + let initial_count = cache.states.len(); + assert!(initial_count > 3); + + // Cleanup to keep only radius of 1 + cache.cleanup(1); + + // Should only keep current month and ±1 month = 3 months + assert_eq!(cache.states.len(), 3); + assert!(cache.states.contains_key(&(2024, 5))); + assert!(cache.states.contains_key(&(2024, 6))); + assert!(cache.states.contains_key(&(2024, 7))); + } + + #[test] + fn test_cleanup_cross_year() { + let mut cache = CalendarCache::new(2024, 1); + cache.precache_surrounding(3, 3); + cache.cleanup(1); + + // Should keep Dec 2023, Jan 2024, Feb 2024 + assert_eq!(cache.states.len(), 3); + assert!(cache.states.contains_key(&(2023, 12))); + assert!(cache.states.contains_key(&(2024, 1))); + assert!(cache.states.contains_key(&(2024, 2))); + } + + // ==================== stats Tests ==================== + + #[test] + fn test_stats() { + let mut cache = CalendarCache::new(2024, 6); + cache.precache_surrounding(1, 1); + + let stats = cache.stats(); + assert_eq!(stats.states_cached, 3); + assert_eq!(stats.period_texts_cached, 3); + assert_eq!(stats.current_month, (2024, 6)); + } + + // ==================== CacheStats Debug Tests ==================== + + #[test] + fn test_cache_stats_debug() { + let stats = CacheStats { + states_cached: 5, + period_texts_cached: 5, + current_month: (2024, 6), + }; + let debug_str = format!("{:?}", stats); + assert!(debug_str.contains("states_cached")); + assert!(debug_str.contains("5")); + } +} diff --git a/src/database/schema.rs b/src/database/schema.rs index 0fe67b5..f94b777 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -378,6 +378,27 @@ mod tests { use crate::caldav::{AlertTime, RepeatFrequency, TravelTime}; use chrono::TimeZone; + fn create_test_event(uid: &str, summary: &str) -> CalendarEvent { + CalendarEvent { + uid: uid.to_string(), + summary: summary.to_string(), + location: None, + all_day: false, + start: Utc.with_ymd_and_hms(2025, 11, 29, 10, 0, 0).unwrap(), + end: Utc.with_ymd_and_hms(2025, 11, 29, 11, 0, 0).unwrap(), + travel_time: TravelTime::None, + repeat: RepeatFrequency::Never, + invitees: vec![], + alert: AlertTime::None, + alert_second: None, + attachments: vec![], + url: None, + notes: None, + } + } + + // ==================== Database Creation Tests ==================== + #[test] fn test_database_creation() { let temp_dir = std::env::temp_dir(); @@ -396,6 +417,28 @@ mod tests { let _ = std::fs::remove_file(&db_path); } + #[test] + fn test_database_path() { + let path = Database::get_database_path(); + assert!(path.to_string_lossy().contains("sol-calendar")); + assert!(path.to_string_lossy().contains("sol.db")); + } + + #[test] + fn test_database_debug() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_debug.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + let debug_str = format!("{:?}", db); + assert!(debug_str.contains("Database")); + + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Event CRUD Tests ==================== + #[test] fn test_event_operations() { let temp_dir = std::env::temp_dir(); @@ -444,4 +487,253 @@ mod tests { // Clean up let _ = std::fs::remove_file(&db_path); } + + #[test] + fn test_insert_event_with_all_fields() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_all_fields.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + let event = CalendarEvent { + uid: "full-event-1".to_string(), + summary: "Full Event".to_string(), + location: Some("Conference Room A".to_string()), + all_day: true, + start: Utc.with_ymd_and_hms(2025, 12, 1, 0, 0, 0).unwrap(), + end: Utc.with_ymd_and_hms(2025, 12, 1, 23, 59, 59).unwrap(), + travel_time: TravelTime::ThirtyMinutes, + repeat: RepeatFrequency::Weekly, + invitees: vec!["alice@example.com".to_string(), "bob@example.com".to_string()], + alert: AlertTime::FifteenMinutes, + alert_second: Some(AlertTime::OneHour), + attachments: vec!["doc.pdf".to_string()], + url: Some("https://example.com/meeting".to_string()), + notes: Some("Important meeting notes".to_string()), + }; + + db.insert_event("work", &event).unwrap(); + + let events = db.get_events_for_calendar("work").unwrap(); + assert_eq!(events.len(), 1); + let retrieved = &events[0]; + + assert_eq!(retrieved.uid, "full-event-1"); + assert_eq!(retrieved.summary, "Full Event"); + assert_eq!(retrieved.location, Some("Conference Room A".to_string())); + assert!(retrieved.all_day); + assert_eq!(retrieved.travel_time, TravelTime::ThirtyMinutes); + assert_eq!(retrieved.repeat, RepeatFrequency::Weekly); + assert_eq!(retrieved.invitees.len(), 2); + assert_eq!(retrieved.alert, AlertTime::FifteenMinutes); + assert!(retrieved.alert_second.is_some()); + assert_eq!(retrieved.attachments.len(), 1); + assert_eq!(retrieved.url, Some("https://example.com/meeting".to_string())); + assert_eq!(retrieved.notes, Some("Important meeting notes".to_string())); + + let _ = std::fs::remove_file(&db_path); + } + + #[test] + fn test_update_event() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_update.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + let mut event = create_test_event("update-1", "Original Title"); + db.insert_event("cal1", &event).unwrap(); + + // Update the event + event.summary = "Updated Title".to_string(); + event.location = Some("New Location".to_string()); + event.notes = Some("Updated notes".to_string()); + db.update_event(&event).unwrap(); + + let events = db.get_events_for_calendar("cal1").unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].summary, "Updated Title"); + assert_eq!(events[0].location, Some("New Location".to_string())); + assert_eq!(events[0].notes, Some("Updated notes".to_string())); + + let _ = std::fs::remove_file(&db_path); + } + + #[test] + fn test_delete_nonexistent_event() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_delete_none.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + let deleted = db.delete_event("nonexistent").unwrap(); + assert!(!deleted); + + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Multiple Events Tests ==================== + + #[test] + fn test_multiple_events_same_calendar() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_multi.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + for i in 1..=5 { + let event = create_test_event(&format!("event-{}", i), &format!("Event {}", i)); + db.insert_event("cal1", &event).unwrap(); + } + + let events = db.get_events_for_calendar("cal1").unwrap(); + assert_eq!(events.len(), 5); + + let _ = std::fs::remove_file(&db_path); + } + + #[test] + fn test_events_multiple_calendars() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_multi_cal.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + // Add events to different calendars + db.insert_event("work", &create_test_event("work-1", "Work Event 1")).unwrap(); + db.insert_event("work", &create_test_event("work-2", "Work Event 2")).unwrap(); + db.insert_event("personal", &create_test_event("personal-1", "Personal Event")).unwrap(); + + // Check each calendar has correct events + let work_events = db.get_events_for_calendar("work").unwrap(); + assert_eq!(work_events.len(), 2); + + let personal_events = db.get_events_for_calendar("personal").unwrap(); + assert_eq!(personal_events.len(), 1); + + // Delete events for one calendar + db.delete_events_for_calendar("work").unwrap(); + + let work_events = db.get_events_for_calendar("work").unwrap(); + assert_eq!(work_events.len(), 0); + + // Personal events should be unaffected + let personal_events = db.get_events_for_calendar("personal").unwrap(); + assert_eq!(personal_events.len(), 1); + + let _ = std::fs::remove_file(&db_path); + } + + // ==================== clear_all_events Tests ==================== + + #[test] + fn test_clear_all_events() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_clear.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + // Add events to multiple calendars + db.insert_event("cal1", &create_test_event("e1", "Event 1")).unwrap(); + db.insert_event("cal2", &create_test_event("e2", "Event 2")).unwrap(); + db.insert_event("cal3", &create_test_event("e3", "Event 3")).unwrap(); + + let count = db.clear_all_events().unwrap(); + assert_eq!(count, 3); + + // All calendars should be empty + assert_eq!(db.get_events_for_calendar("cal1").unwrap().len(), 0); + assert_eq!(db.get_events_for_calendar("cal2").unwrap().len(), 0); + assert_eq!(db.get_events_for_calendar("cal3").unwrap().len(), 0); + + let _ = std::fs::remove_file(&db_path); + } + + #[test] + fn test_clear_all_events_empty_db() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_clear_empty.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + let count = db.clear_all_events().unwrap(); + assert_eq!(count, 0); + + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Unicode and Special Characters Tests ==================== + + #[test] + fn test_unicode_event_data() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_unicode.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + let event = CalendarEvent { + uid: "unicode-1".to_string(), + summary: "会议 Meeting 📅".to_string(), + location: Some("北京 Beijing 🌏".to_string()), + all_day: false, + start: Utc.with_ymd_and_hms(2025, 12, 1, 10, 0, 0).unwrap(), + end: Utc.with_ymd_and_hms(2025, 12, 1, 11, 0, 0).unwrap(), + travel_time: TravelTime::None, + repeat: RepeatFrequency::Never, + invitees: vec!["用户@example.com".to_string()], + alert: AlertTime::None, + alert_second: None, + attachments: vec!["文档.pdf".to_string()], + url: Some("https://example.com/会议".to_string()), + notes: Some("备注 Notes 🗒️".to_string()), + }; + + db.insert_event("cal1", &event).unwrap(); + + let events = db.get_events_for_calendar("cal1").unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].summary, "会议 Meeting 📅"); + assert_eq!(events[0].location, Some("北京 Beijing 🌏".to_string())); + assert_eq!(events[0].notes, Some("备注 Notes 🗒️".to_string())); + + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Empty Calendar Tests ==================== + + #[test] + fn test_get_events_empty_calendar() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_empty_cal.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + let events = db.get_events_for_calendar("nonexistent").unwrap(); + assert_eq!(events.len(), 0); + + let _ = std::fs::remove_file(&db_path); + } + + #[test] + fn test_delete_events_empty_calendar() { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("sol_test_del_empty.db"); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + + let count = db.delete_events_for_calendar("nonexistent").unwrap(); + assert_eq!(count, 0); + + let _ = std::fs::remove_file(&db_path); + } } diff --git a/src/locale.rs b/src/locale.rs index 662db4b..eb4d595 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -305,7 +305,33 @@ fn detect_date_format(locale: &str) -> DateFormat { #[cfg(test)] mod tests { use super::*; - use chrono::Weekday; + use chrono::{NaiveDate, Weekday}; + + // ==================== DateFormat Tests ==================== + + #[test] + fn test_date_format_variants() { + // Ensure all variants are distinct + assert_ne!(DateFormat::DMY, DateFormat::MDY); + assert_ne!(DateFormat::MDY, DateFormat::YMD); + assert_ne!(DateFormat::DMY, DateFormat::YMD); + } + + #[test] + fn test_date_format_debug() { + assert_eq!(format!("{:?}", DateFormat::DMY), "DMY"); + assert_eq!(format!("{:?}", DateFormat::MDY), "MDY"); + assert_eq!(format!("{:?}", DateFormat::YMD), "YMD"); + } + + #[test] + fn test_date_format_clone() { + let format = DateFormat::DMY; + let cloned = format.clone(); + assert_eq!(format, cloned); + } + + // ==================== 24-Hour Format Detection Tests ==================== #[test] fn test_24_hour_detection() { @@ -316,6 +342,30 @@ mod tests { assert_eq!(detect_24_hour_format("en_CA.UTF-8"), false); } + #[test] + fn test_24_hour_detection_more_locales() { + // 24-hour locales + assert!(detect_24_hour_format("es_ES.UTF-8")); + assert!(detect_24_hour_format("it_IT.UTF-8")); + assert!(detect_24_hour_format("pl_PL.UTF-8")); + assert!(detect_24_hour_format("ru_RU.UTF-8")); + assert!(detect_24_hour_format("sv_SE.UTF-8")); + + // 12-hour locales + assert!(!detect_24_hour_format("en_AU.UTF-8")); + assert!(!detect_24_hour_format("en_NZ.UTF-8")); + assert!(!detect_24_hour_format("en_PH.UTF-8")); + assert!(!detect_24_hour_format("fil_PH.UTF-8")); + } + + #[test] + fn test_24_hour_case_insensitive() { + assert!(!detect_24_hour_format("EN_US.UTF-8")); + assert!(!detect_24_hour_format("En_Us.utf-8")); + } + + // ==================== First Day of Week Detection Tests ==================== + #[test] fn test_first_day_detection() { assert_eq!(detect_first_day_of_week("de_DE.UTF-8"), Weekday::Mon); @@ -325,6 +375,54 @@ mod tests { assert_eq!(detect_first_day_of_week("ar_SA.UTF-8"), Weekday::Sun); } + #[test] + fn test_first_day_more_locales() { + // Monday-starting + assert_eq!(detect_first_day_of_week("fr_FR.UTF-8"), Weekday::Mon); + assert_eq!(detect_first_day_of_week("es_ES.UTF-8"), Weekday::Mon); + assert_eq!(detect_first_day_of_week("it_IT.UTF-8"), Weekday::Mon); + + // Sunday-starting + assert_eq!(detect_first_day_of_week("zh_CN.UTF-8"), Weekday::Sun); + assert_eq!(detect_first_day_of_week("ko_KR.UTF-8"), Weekday::Sun); + assert_eq!(detect_first_day_of_week("pt_BR.UTF-8"), Weekday::Sun); + + // Saturday-starting (Middle Eastern) + assert_eq!(detect_first_day_of_week("ar_IQ.UTF-8"), Weekday::Sat); + assert_eq!(detect_first_day_of_week("ar_SY.UTF-8"), Weekday::Sat); + } + + // ==================== Date Format Detection Tests ==================== + + #[test] + fn test_date_format_detection() { + assert_eq!(detect_date_format("en_US.UTF-8"), DateFormat::MDY); + assert_eq!(detect_date_format("en_GB.UTF-8"), DateFormat::DMY); + assert_eq!(detect_date_format("de_DE.UTF-8"), DateFormat::DMY); + assert_eq!(detect_date_format("ja_JP.UTF-8"), DateFormat::YMD); + assert_eq!(detect_date_format("zh_CN.UTF-8"), DateFormat::YMD); + assert_eq!(detect_date_format("ko_KR.UTF-8"), DateFormat::YMD); + } + + #[test] + fn test_date_format_more_locales() { + // MDY locales + assert_eq!(detect_date_format("en_CA.UTF-8"), DateFormat::MDY); + assert_eq!(detect_date_format("en_PH.UTF-8"), DateFormat::MDY); + + // DMY locales (most of the world) + assert_eq!(detect_date_format("fr_FR.UTF-8"), DateFormat::DMY); + assert_eq!(detect_date_format("es_ES.UTF-8"), DateFormat::DMY); + assert_eq!(detect_date_format("pt_PT.UTF-8"), DateFormat::DMY); + assert_eq!(detect_date_format("ru_RU.UTF-8"), DateFormat::DMY); + + // YMD locales + assert_eq!(detect_date_format("zh_TW.UTF-8"), DateFormat::YMD); + assert_eq!(detect_date_format("hu_HU.UTF-8"), DateFormat::YMD); + } + + // ==================== Hour Formatting Tests ==================== + #[test] fn test_hour_formatting() { let locale_24h = LocalePreferences { @@ -352,12 +450,249 @@ mod tests { } #[test] - fn test_date_format_detection() { - assert_eq!(detect_date_format("en_US.UTF-8"), DateFormat::MDY); - assert_eq!(detect_date_format("en_GB.UTF-8"), DateFormat::DMY); - assert_eq!(detect_date_format("de_DE.UTF-8"), DateFormat::DMY); - assert_eq!(detect_date_format("ja_JP.UTF-8"), DateFormat::YMD); - assert_eq!(detect_date_format("zh_CN.UTF-8"), DateFormat::YMD); - assert_eq!(detect_date_format("ko_KR.UTF-8"), DateFormat::YMD); + fn test_hour_formatting_all_hours_24h() { + let locale = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "de_DE.UTF-8".to_string(), + }; + + for hour in 0..24 { + let formatted = locale.format_hour(hour); + assert!(formatted.ends_with(":00")); + assert!(formatted.len() == 5); // "HH:00" + } + } + + #[test] + fn test_hour_formatting_all_hours_12h() { + let locale = LocalePreferences { + use_24_hour: false, + first_day_of_week: Weekday::Sun, + date_format: DateFormat::MDY, + locale_string: "en_US.UTF-8".to_string(), + }; + + for hour in 0..24 { + let formatted = locale.format_hour(hour); + assert!(formatted.contains("AM") || formatted.contains("PM")); + } + } + + // ==================== days_from_monday Tests ==================== + + #[test] + fn test_days_from_monday() { + let locale_mon = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "de_DE.UTF-8".to_string(), + }; + assert_eq!(locale_mon.days_from_monday(), 0); + + let locale_sun = LocalePreferences { + use_24_hour: false, + first_day_of_week: Weekday::Sun, + date_format: DateFormat::MDY, + locale_string: "en_US.UTF-8".to_string(), + }; + assert_eq!(locale_sun.days_from_monday(), -6); + + let locale_sat = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Sat, + date_format: DateFormat::DMY, + locale_string: "ar_IQ.UTF-8".to_string(), + }; + assert_eq!(locale_sat.days_from_monday(), -5); + } + + // ==================== is_weekend Tests ==================== + + #[test] + fn test_is_weekend() { + let locale = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "de_DE.UTF-8".to_string(), + }; + + assert!(locale.is_weekend(Weekday::Sat)); + assert!(locale.is_weekend(Weekday::Sun)); + assert!(!locale.is_weekend(Weekday::Mon)); + assert!(!locale.is_weekend(Weekday::Tue)); + assert!(!locale.is_weekend(Weekday::Wed)); + assert!(!locale.is_weekend(Weekday::Thu)); + assert!(!locale.is_weekend(Weekday::Fri)); + } + + // ==================== format_week_range Tests ==================== + + #[test] + fn test_format_week_range_mdy_same_month() { + let locale = LocalePreferences { + use_24_hour: false, + first_day_of_week: Weekday::Sun, + date_format: DateFormat::MDY, + locale_string: "en_US.UTF-8".to_string(), + }; + + let first = NaiveDate::from_ymd_opt(2024, 11, 24).unwrap(); + let last = NaiveDate::from_ymd_opt(2024, 11, 30).unwrap(); + + let result = locale.format_week_range(&first, &last, 48); + assert!(result.contains("W48")); + assert!(result.contains("Nov")); + assert!(result.contains("24")); + assert!(result.contains("30")); + } + + #[test] + fn test_format_week_range_dmy_same_month() { + let locale = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "en_GB.UTF-8".to_string(), + }; + + let first = NaiveDate::from_ymd_opt(2024, 11, 25).unwrap(); + let last = NaiveDate::from_ymd_opt(2024, 11, 30).unwrap(); + + let result = locale.format_week_range(&first, &last, 48); + assert!(result.contains("W48")); + assert!(result.contains("25")); + assert!(result.contains("30")); + } + + #[test] + fn test_format_week_range_ymd() { + let locale = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Sun, + date_format: DateFormat::YMD, + locale_string: "ja_JP.UTF-8".to_string(), + }; + + let first = NaiveDate::from_ymd_opt(2024, 11, 24).unwrap(); + let last = NaiveDate::from_ymd_opt(2024, 11, 30).unwrap(); + + let result = locale.format_week_range(&first, &last, 48); + assert!(result.contains("W48")); + assert!(result.contains("2024")); + } + + // ==================== format_day_header Tests ==================== + + #[test] + fn test_format_day_header_mdy() { + let locale = LocalePreferences { + use_24_hour: false, + first_day_of_week: Weekday::Sun, + date_format: DateFormat::MDY, + locale_string: "en_US.UTF-8".to_string(), + }; + + let date = NaiveDate::from_ymd_opt(2024, 11, 25).unwrap(); + let result = locale.format_day_header(&date, "Monday"); + + assert!(result.contains("Monday")); + assert!(result.contains("Nov")); + assert!(result.contains("25")); + } + + #[test] + fn test_format_day_header_dmy() { + let locale = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "en_GB.UTF-8".to_string(), + }; + + let date = NaiveDate::from_ymd_opt(2024, 11, 25).unwrap(); + let result = locale.format_day_header(&date, "Monday"); + + assert!(result.contains("Monday")); + assert!(result.contains("25")); + assert!(result.contains("Nov")); + } + + #[test] + fn test_format_day_header_ymd() { + let locale = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Sun, + date_format: DateFormat::YMD, + locale_string: "ja_JP.UTF-8".to_string(), + }; + + let date = NaiveDate::from_ymd_opt(2024, 11, 25).unwrap(); + let result = locale.format_day_header(&date, "Monday"); + + assert!(result.contains("Monday")); + assert!(result.contains("2024-11-25")); + } + + // ==================== LocalePreferences Tests ==================== + + #[test] + fn test_locale_preferences_clone() { + let locale = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "de_DE.UTF-8".to_string(), + }; + + let cloned = locale.clone(); + assert_eq!(locale.use_24_hour, cloned.use_24_hour); + assert_eq!(locale.first_day_of_week, cloned.first_day_of_week); + assert_eq!(locale.date_format, cloned.date_format); + assert_eq!(locale.locale_string, cloned.locale_string); + } + + #[test] + fn test_locale_preferences_debug() { + let locale = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "de_DE.UTF-8".to_string(), + }; + + let debug_str = format!("{:?}", locale); + assert!(debug_str.contains("LocalePreferences")); + assert!(debug_str.contains("use_24_hour")); + } + + #[test] + fn test_locale_preferences_partial_eq() { + let locale1 = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "de_DE.UTF-8".to_string(), + }; + + let locale2 = LocalePreferences { + use_24_hour: true, + first_day_of_week: Weekday::Mon, + date_format: DateFormat::DMY, + locale_string: "de_DE.UTF-8".to_string(), + }; + + let locale3 = LocalePreferences { + use_24_hour: false, + first_day_of_week: Weekday::Sun, + date_format: DateFormat::MDY, + locale_string: "en_US.UTF-8".to_string(), + }; + + assert_eq!(locale1, locale2); + assert_ne!(locale1, locale3); } } diff --git a/src/protocols/local.rs b/src/protocols/local.rs index f343226..d1bc1a9 100644 --- a/src/protocols/local.rs +++ b/src/protocols/local.rs @@ -64,6 +64,37 @@ mod tests { use crate::caldav::{AlertTime, RepeatFrequency, TravelTime}; use chrono::{TimeZone, Utc}; + fn create_test_event(uid: &str, summary: &str) -> CalendarEvent { + CalendarEvent { + uid: uid.to_string(), + summary: summary.to_string(), + location: None, + all_day: false, + start: Utc.with_ymd_and_hms(2025, 11, 30, 10, 0, 0).unwrap(), + end: Utc.with_ymd_and_hms(2025, 11, 30, 11, 0, 0).unwrap(), + travel_time: TravelTime::None, + repeat: RepeatFrequency::Never, + invitees: vec![], + alert: AlertTime::None, + alert_second: None, + attachments: vec![], + url: None, + notes: None, + } + } + + fn setup_protocol() -> (LocalProtocol, std::path::PathBuf) { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join(format!("sol_protocol_test_{}.db", std::process::id())); + let _ = std::fs::remove_file(&db_path); + + let db = Database::open_at(db_path.clone()).unwrap(); + let db = Arc::new(Mutex::new(db)); + (LocalProtocol::new(db), db_path) + } + + // ==================== Basic Protocol Tests ==================== + #[test] fn test_local_protocol() { let temp_dir = std::env::temp_dir(); @@ -111,4 +142,137 @@ mod tests { // Clean up let _ = std::fs::remove_file(&db_path); } + + // ==================== Protocol Trait Tests ==================== + + #[test] + fn test_protocol_type() { + let (protocol, db_path) = setup_protocol(); + assert_eq!(protocol.protocol_type(), "local"); + let _ = std::fs::remove_file(&db_path); + } + + #[test] + fn test_requires_network() { + let (protocol, db_path) = setup_protocol(); + assert!(!protocol.requires_network()); + let _ = std::fs::remove_file(&db_path); + } + + #[test] + fn test_sync_local_protocol() { + let (mut protocol, db_path) = setup_protocol(); + // Sync should be a no-op for local protocol + let result = protocol.sync("test-cal"); + assert!(result.is_ok()); + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Update Event Tests ==================== + + #[test] + fn test_update_event() { + let (mut protocol, db_path) = setup_protocol(); + + let mut event = create_test_event("update-1", "Original"); + protocol.add_event("cal1", &event).unwrap(); + + // Update + event.summary = "Updated".to_string(); + event.location = Some("New Location".to_string()); + protocol.update_event("cal1", &event).unwrap(); + + let events = protocol.fetch_events("cal1").unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].summary, "Updated"); + assert_eq!(events[0].location, Some("New Location".to_string())); + + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Multiple Events Tests ==================== + + #[test] + fn test_multiple_events() { + let (mut protocol, db_path) = setup_protocol(); + + for i in 1..=5 { + let event = create_test_event(&format!("event-{}", i), &format!("Event {}", i)); + protocol.add_event("cal1", &event).unwrap(); + } + + let events = protocol.fetch_events("cal1").unwrap(); + assert_eq!(events.len(), 5); + + let _ = std::fs::remove_file(&db_path); + } + + #[test] + fn test_multiple_calendars() { + let (mut protocol, db_path) = setup_protocol(); + + protocol.add_event("work", &create_test_event("w1", "Work 1")).unwrap(); + protocol.add_event("work", &create_test_event("w2", "Work 2")).unwrap(); + protocol.add_event("personal", &create_test_event("p1", "Personal")).unwrap(); + + assert_eq!(protocol.fetch_events("work").unwrap().len(), 2); + assert_eq!(protocol.fetch_events("personal").unwrap().len(), 1); + assert_eq!(protocol.fetch_events("other").unwrap().len(), 0); + + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Delete Tests ==================== + + #[test] + fn test_delete_nonexistent() { + let (mut protocol, db_path) = setup_protocol(); + + let deleted = protocol.delete_event("cal1", "nonexistent").unwrap(); + assert!(!deleted); + + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Debug Tests ==================== + + #[test] + fn test_local_protocol_debug() { + let (protocol, db_path) = setup_protocol(); + let debug_str = format!("{:?}", protocol); + assert!(debug_str.contains("LocalProtocol")); + let _ = std::fs::remove_file(&db_path); + } + + // ==================== Unicode Tests ==================== + + #[test] + fn test_unicode_events() { + let (mut protocol, db_path) = setup_protocol(); + + let event = CalendarEvent { + uid: "unicode-1".to_string(), + summary: "会议 📅".to_string(), + location: Some("北京".to_string()), + all_day: false, + start: Utc.with_ymd_and_hms(2025, 12, 1, 10, 0, 0).unwrap(), + end: Utc.with_ymd_and_hms(2025, 12, 1, 11, 0, 0).unwrap(), + travel_time: TravelTime::None, + repeat: RepeatFrequency::Never, + invitees: vec![], + alert: AlertTime::None, + alert_second: None, + attachments: vec![], + url: None, + notes: Some("备注".to_string()), + }; + + protocol.add_event("cal1", &event).unwrap(); + + let events = protocol.fetch_events("cal1").unwrap(); + assert_eq!(events[0].summary, "会议 📅"); + assert_eq!(events[0].location, Some("北京".to_string())); + + let _ = std::fs::remove_file(&db_path); + } } diff --git a/src/validation.rs b/src/validation.rs index 89b03d6..97c634a 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -40,24 +40,134 @@ pub fn ensure_end_after_start(start: NaiveDate, end: NaiveDate) -> NaiveDate { mod tests { use super::*; + // ==================== parse_date Tests ==================== + #[test] - fn test_parse_date() { + fn test_parse_date_valid() { assert!(parse_date("2024-01-15").is_some()); + assert!(parse_date("2024-12-31").is_some()); + assert!(parse_date("2024-02-29").is_some()); // Leap year + assert!(parse_date("1999-01-01").is_some()); + assert!(parse_date("2099-12-31").is_some()); + } + + #[test] + fn test_parse_date_invalid_format() { assert!(parse_date("invalid").is_none()); - assert!(parse_date("01-15-2024").is_none()); + assert!(parse_date("01-15-2024").is_none()); // US format + assert!(parse_date("15/01/2024").is_none()); // European format + assert!(parse_date("2024/01/15").is_none()); // Slash separator + assert!(parse_date("").is_none()); + assert!(parse_date(" ").is_none()); + } + + #[test] + fn test_parse_date_invalid_values() { + assert!(parse_date("2024-13-01").is_none()); // Invalid month + assert!(parse_date("2024-00-01").is_none()); // Month 0 + assert!(parse_date("2024-01-32").is_none()); // Day 32 + assert!(parse_date("2024-02-30").is_none()); // Feb 30 + assert!(parse_date("2023-02-29").is_none()); // Not a leap year + } + + #[test] + fn test_parse_date_returns_correct_value() { + let date = parse_date("2024-06-15").unwrap(); + assert_eq!(date.year(), 2024); + assert_eq!(date.month(), 6); + assert_eq!(date.day(), 15); } + // ==================== validate_event_title Tests ==================== + #[test] - fn test_validate_event_title() { + fn test_validate_event_title_valid() { assert!(validate_event_title("Meeting")); + assert!(validate_event_title("A")); // Single char + assert!(validate_event_title("会议")); // Chinese + assert!(validate_event_title("Meeting 📅")); // With emoji + assert!(validate_event_title(" Meeting ")); // Leading/trailing whitespace (trimmed) + } + + #[test] + fn test_validate_event_title_invalid() { assert!(!validate_event_title("")); assert!(!validate_event_title(" ")); + assert!(!validate_event_title("\t")); + assert!(!validate_event_title("\n")); + assert!(!validate_event_title("\t\n \t")); + } + + #[test] + fn test_validate_event_title_edge_cases() { + assert!(validate_event_title("Line1\nLine2")); // Multiline + assert!(validate_event_title("Very long title ".repeat(100).as_str())); // Long title + assert!(validate_event_title("Special chars: !@#$%^&*()")); } + // ==================== validate_email Tests ==================== + #[test] - fn test_validate_email() { + fn test_validate_email_valid() { assert!(validate_email("test@example.com")); + assert!(validate_email("user.name@domain.co.uk")); + assert!(validate_email("a@b.c")); + assert!(validate_email(" test@example.com ")); // With whitespace (trimmed) + assert!(validate_email("user+tag@example.com")); // Plus addressing + } + + #[test] + fn test_validate_email_invalid() { assert!(!validate_email("invalid")); assert!(!validate_email("")); + assert!(!validate_email(" ")); + assert!(!validate_email("@example.com")); // Missing local part + assert!(!validate_email("test@")); // Missing domain + assert!(!validate_email("test@example")); // Missing TLD dot + assert!(!validate_email("testexample.com")); // Missing @ + } + + #[test] + fn test_validate_email_edge_cases() { + // Note: This is a basic validator, not RFC 5322 compliant + assert!(validate_email("a.b@c.d")); // Minimal valid + assert!(!validate_email("test@localhost")); // No dot in domain (fails basic check) + } + + // ==================== ensure_end_after_start Tests ==================== + + #[test] + fn test_ensure_end_after_start_valid() { + let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(); + let end = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(); + assert_eq!(ensure_end_after_start(start, end), end); + } + + #[test] + fn test_ensure_end_after_start_same_day() { + let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(); + assert_eq!(ensure_end_after_start(date, date), date); + } + + #[test] + fn test_ensure_end_after_start_end_before_start() { + let start = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(); + let end = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(); + // When end < start, should return start + assert_eq!(ensure_end_after_start(start, end), start); + } + + #[test] + fn test_ensure_end_after_start_cross_year() { + let start = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(); + let end = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(); + assert_eq!(ensure_end_after_start(start, end), end); + } + + #[test] + fn test_ensure_end_after_start_cross_year_invalid() { + let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(); + let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(); + assert_eq!(ensure_end_after_start(start, end), start); } }