caldavpuller/tests/integration_tests.rs
Alvaro Soliverez 640ae119d1 feat: Complete import functionality with RRULE fixes and comprehensive testing
- Fix RRULE BYDAY filtering for daily frequency events (Tether sync weekdays only)
- Fix timezone transfer in recurring event expansion
- Add comprehensive timezone-aware iCal generation
- Add extensive test suite for recurrence and timezone functionality
- Update dependencies and configuration examples
- Implement cleanup logic for orphaned events
- Add detailed import plan documentation

This completes the core import functionality with proper timezone handling,
RRULE parsing, and duplicate prevention mechanisms.
2025-11-21 12:04:46 -03:00

556 lines
20 KiB
Rust

use caldav_sync::{Config, CalDavResult};
#[cfg(test)]
mod config_tests {
use super::*;
#[test]
fn test_default_config() -> CalDavResult<()> {
let config = Config::default();
assert_eq!(config.server.url, "https://caldav.example.com");
assert_eq!(config.calendar.name, "calendar");
assert_eq!(config.sync.interval, 300);
config.validate()?;
Ok(())
}
#[test]
fn test_config_validation() -> CalDavResult<()> {
let mut config = Config::default();
// Should fail with empty credentials
assert!(config.validate().is_err());
config.server.username = "test_user".to_string();
config.server.password = "test_pass".to_string();
// Should succeed now
assert!(config.validate().is_ok());
Ok(())
}
}
#[cfg(test)]
mod error_tests {
use caldav_sync::{CalDavError, CalDavResult};
#[test]
fn test_error_retryable() {
let network_error = CalDavError::Network(
reqwest::Error::from(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "test"))
);
assert!(network_error.is_retryable());
let auth_error = CalDavError::Authentication("Invalid credentials".to_string());
assert!(!auth_error.is_retryable());
let config_error = CalDavError::Config("Missing URL".to_string());
assert!(!config_error.is_retryable());
}
#[test]
fn test_error_classification() {
let auth_error = CalDavError::Authentication("Invalid".to_string());
assert!(auth_error.is_auth_error());
let config_error = CalDavError::Config("Invalid".to_string());
assert!(config_error.is_config_error());
}
}
#[cfg(test)]
mod event_tests {
use caldav_sync::event::{Event, EventStatus, EventType};
use chrono::{DateTime, Utc, NaiveDate};
#[test]
fn test_event_creation() {
let start = Utc::now();
let end = start + chrono::Duration::hours(1);
let event = Event::new("Test Event".to_string(), start, end);
assert_eq!(event.summary, "Test Event");
assert_eq!(event.start, start);
assert_eq!(event.end, end);
assert!(!event.all_day);
assert_eq!(event.status, EventStatus::Confirmed);
assert_eq!(event.event_type, EventType::Public);
}
#[test]
fn test_all_day_event() {
let date = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
let event = Event::new_all_day("Christmas".to_string(), date);
assert_eq!(event.summary, "Christmas");
assert!(event.all_day);
assert!(event.occurs_on(date));
}
#[test]
fn test_event_to_ical() -> caldav_sync::CalDavResult<()> {
let event = Event::new(
"Meeting".to_string(),
DateTime::from_naive_utc_and_offset(
chrono::NaiveDateTime::parse_from_str("20231225T100000", "%Y%m%dT%H%M%S").unwrap(),
Utc
),
DateTime::from_naive_utc_and_offset(
chrono::NaiveDateTime::parse_from_str("20231225T110000", "%Y%m%dT%H%M%S").unwrap(),
Utc
),
);
let ical = event.to_ical()?;
assert!(ical.contains("SUMMARY:Meeting"));
assert!(ical.contains("DTSTART:20231225T100000Z"));
assert!(ical.contains("DTEND:20231225T110000Z"));
assert!(ical.contains("BEGIN:VCALENDAR"));
assert!(ical.contains("END:VCALENDAR"));
Ok(())
}
}
#[cfg(test)]
mod timezone_tests {
use caldav_sync::timezone::TimezoneHandler;
#[test]
fn test_timezone_handler_creation() -> CalDavResult<()> {
let handler = TimezoneHandler::new("UTC")?;
assert_eq!(handler.default_timezone(), "UTC");
Ok(())
}
#[test]
fn test_timezone_validation() {
assert!(TimezoneHandler::validate_timezone("UTC"));
assert!(TimezoneHandler::validate_timezone("America/New_York"));
assert!(TimezoneHandler::validate_timezone("Europe/London"));
assert!(!TimezoneHandler::validate_timezone("Invalid/Timezone"));
}
#[test]
fn test_ical_formatting() -> CalDavResult<()> {
let handler = TimezoneHandler::default();
let dt = DateTime::from_naive_utc_and_offset(
chrono::NaiveDateTime::parse_from_str("20231225T100000", "%Y%m%dT%H%M%S").unwrap(),
Utc
);
let ical_utc = handler.format_ical_datetime(dt, false)?;
assert_eq!(ical_utc, "20231225T100000Z");
let ical_date = handler.format_ical_date(dt);
assert_eq!(ical_date, "20231225");
Ok(())
}
}
#[cfg(test)]
mod filter_tests {
use caldav_sync::calendar_filter::{
CalendarFilter, FilterRule, DateRangeFilter, KeywordFilter,
EventTypeFilter, EventStatusFilter, FilterBuilder
};
use caldav_sync::event::{Event, EventStatus, EventType};
use chrono::{DateTime, Utc};
#[test]
fn test_date_range_filter() {
let start = DateTime::from_naive_utc_and_offset(
chrono::NaiveDateTime::parse_from_str("20231225T000000", "%Y%m%dT%H%M%S").unwrap(),
Utc
);
let end = DateTime::from_naive_utc_and_offset(
chrono::NaiveDateTime::parse_from_str("20231225T235959", "%Y%m%dT%H%M%S").unwrap(),
Utc
);
let filter = DateRangeFilter::new(start, end);
let event_start = DateTime::from_naive_utc_and_offset(
chrono::NaiveDateTime::parse_from_str("20231225T100000", "%Y%m%dT%H%M%S").unwrap(),
Utc
);
let event = Event::new("Test".to_string(), event_start, event_start + chrono::Duration::hours(1));
assert!(filter.matches_event(&event));
let event_outside = Event::new(
"Test".to_string(),
start - chrono::Duration::days(1),
start - chrono::Duration::hours(23),
);
assert!(!filter_outside.matches_event(&event_outside));
}
#[test]
fn test_keyword_filter() {
let filter = KeywordFilter::new(vec!["meeting".to_string(), "important".to_string()], false);
let event1 = Event::new("Team Meeting".to_string(), Utc::now(), Utc::now());
assert!(filter.matches_event(&event1));
let event2 = Event::new("Lunch".to_string(), Utc::now(), Utc::now());
assert!(!filter.matches_event(&event2));
}
#[test]
fn test_calendar_filter() {
let mut filter = CalendarFilter::new(true); // OR logic
filter.add_rule(FilterRule::Keywords(KeywordFilter::new(vec!["meeting".to_string()], false)));
filter.add_rule(FilterRule::EventStatus(EventStatusFilter::new(vec![EventStatus::Cancelled])));
let event1 = Event::new("Team Meeting".to_string(), Utc::now(), Utc::now());
assert!(filter.matches_event(&event1)); // Matches keyword
let mut event2 = Event::new("Holiday".to_string(), Utc::now(), Utc::now());
event2.status = EventStatus::Cancelled;
assert!(filter.matches_event(&event2)); // Matches status
let event3 = Event::new("Lunch".to_string(), Utc::now(), Utc::now());
assert!(!filter.matches_event(&event3)); // Matches neither
}
#[test]
fn test_filter_builder() {
let filter = FilterBuilder::new()
.match_any(false) // AND logic
.keywords(vec!["meeting".to_string()])
.event_types(vec![EventType::Public])
.build();
let event = Event::new("Team Meeting".to_string(), Utc::now(), Utc::now());
assert!(filter.matches_event(&event)); // Matches both conditions
}
}
#[cfg(test)]
mod live_caldav_tests {
use caldav_sync::Config;
use caldav_sync::minicaldav_client::RealCalDavClient;
use caldav_sync::event::Event;
use chrono::{DateTime, Utc, Duration};
use tokio;
use std::path::PathBuf;
/// Test basic CRUD operations on the import calendar using the test configuration
#[tokio::test]
async fn test_create_update_delete_event() -> Result<(), Box<dyn std::error::Error>> {
println!("🧪 Starting CRUD test with import calendar...");
// Load test configuration
let config_path = PathBuf::from("config-test-import.toml");
let config = Config::from_file(&config_path)?;
// Validate configuration
config.validate()?;
// Create CalDAV client for target server (Nextcloud)
let import_config = config.get_import_config().ok_or("No import configuration found")?;
let target_client = RealCalDavClient::new(
&import_config.target_server.url,
&import_config.target_server.username,
&import_config.target_server.password,
).await?;
// Build target calendar URL
let target_calendar_url = format!("{}/", import_config.target_server.url.trim_end_matches('/'));
// Validate target calendar
let is_valid = target_client.validate_target_calendar(&target_calendar_url).await?;
assert!(is_valid, "Target calendar should be accessible");
println!("✅ Target calendar is accessible");
// Create test event for today
let now = Utc::now();
let today_start = now.date_naive().and_hms_opt(10, 0, 0).unwrap().and_utc();
let today_end = today_start + Duration::hours(1);
let test_uid = format!("test-event-{}", now.timestamp());
let mut test_event = Event::new(
format!("Test Event {}", test_uid),
today_start,
today_end,
);
test_event.uid = test_uid.clone();
test_event.description = Some("This is a test event for CRUD operations".to_string());
test_event.location = Some("Test Location".to_string());
println!("📝 Creating test event: {}", test_event.summary);
// Convert event to iCalendar format
let ical_data = test_event.to_ical()?;
// Test 1: Create event
let create_result = target_client.put_event(
&target_calendar_url,
&test_uid,
&ical_data,
None // No ETag for creation
).await;
match create_result {
Ok(_) => println!("✅ Event created successfully"),
Err(e) => {
println!("❌ Failed to create event: {}", e);
return Err(e.into());
}
}
// Wait a moment to ensure the event is processed
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Test 2: Verify event exists
println!("🔍 Verifying event exists...");
let etag_result = target_client.get_event_etag(&target_calendar_url, &test_uid).await;
let original_etag = match etag_result {
Ok(Some(etag)) => {
println!("✅ Event verified, ETag: {}", etag);
etag
}
Ok(None) => {
println!("❌ Event not found after creation");
return Err("Event not found after creation".into());
}
Err(e) => {
println!("❌ Failed to verify event: {}", e);
return Err(e.into());
}
}
// Test 3: Update event (change date to tomorrow)
println!("📝 Updating event for tomorrow...");
let tomorrow_start = today_start + Duration::days(1);
let tomorrow_end = tomorrow_start + Duration::hours(1);
test_event.start = tomorrow_start;
test_event.end = tomorrow_end;
test_event.summary = format!("Test Event {} (Updated for Tomorrow)", test_uid);
test_event.description = Some("This event has been updated to tomorrow".to_string());
test_event.sequence += 1; // Increment sequence for update
// Convert updated event to iCalendar format
let updated_ical_data = test_event.to_ical()?;
let update_result = target_client.put_event(
&target_calendar_url,
&test_uid,
&updated_ical_data,
Some(&original_etag) // Use ETag for update
).await;
match update_result {
Ok(_) => println!("✅ Event updated successfully"),
Err(e) => {
println!("❌ Failed to update event: {}", e);
return Err(e.into());
}
}
// Wait a moment to ensure the update is processed
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Test 4: Verify event was updated (ETag should change)
println!("🔍 Verifying event update...");
let new_etag_result = target_client.get_event_etag(&target_calendar_url, &test_uid).await;
match new_etag_result {
Ok(Some(new_etag)) => {
if new_etag != original_etag {
println!("✅ Event updated, new ETag: {}", new_etag);
} else {
println!("⚠️ Event ETag didn't change after update");
}
}
Ok(None) => {
println!("❌ Event not found after update");
return Err("Event not found after update".into());
}
Err(e) => {
println!("❌ Failed to verify updated event: {}", e);
return Err(e.into());
}
}
// Test 5: Delete event
println!("🗑️ Deleting event...");
let delete_result = target_client.delete_event(
&target_calendar_url,
&test_uid,
None // No ETag for deletion (let server handle it)
).await;
match delete_result {
Ok(_) => println!("✅ Event deleted successfully"),
Err(e) => {
println!("❌ Failed to delete event: {}", e);
return Err(e.into());
}
}
// Wait a moment to ensure the deletion is processed
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Test 6: Verify event was deleted
println!("🔍 Verifying event deletion...");
let final_check = target_client.get_event_etag(&target_calendar_url, &test_uid).await;
match final_check {
Ok(None) => println!("✅ Event successfully deleted"),
Ok(Some(etag)) => {
println!("❌ Event still exists after deletion, ETag: {}", etag);
return Err("Event still exists after deletion".into());
}
Err(e) => {
println!("❌ Failed to verify deletion: {}", e);
return Err(e.into());
}
}
println!("🎉 All CRUD operations completed successfully!");
Ok(())
}
/// Test HTTP error handling by attempting to delete a non-existent event
#[tokio::test]
async fn test_delete_nonexistent_event() -> Result<(), Box<dyn std::error::Error>> {
println!("🧪 Testing deletion of non-existent event...");
// Load test configuration
let config_path = PathBuf::from("config-test-import.toml");
let config = Config::from_file(&config_path)?;
// Create CalDAV client for target server
let import_config = config.get_import_config().ok_or("No import configuration found")?;
let target_client = RealCalDavClient::new(
&import_config.target_server.url,
&import_config.target_server.username,
&import_config.target_server.password,
).await?;
// Build target calendar URL
let target_calendar_url = format!("{}/", import_config.target_server.url.trim_end_matches('/'));
// Try to delete a non-existent event
let fake_uid = "non-existent-event-12345";
println!("🗑️ Testing deletion of non-existent event: {}", fake_uid);
let delete_result = target_client.delete_event(
&target_calendar_url,
fake_uid,
None
).await;
match delete_result {
Ok(_) => {
println!("✅ Non-existent event deletion handled gracefully (idempotent)");
Ok(())
}
Err(e) => {
println!("❌ Failed to handle non-existent event deletion gracefully: {}", e);
Err(e.into())
}
}
}
/// Test event existence checking
#[tokio::test]
async fn test_event_existence_check() -> Result<(), Box<dyn std::error::Error>> {
println!("🧪 Testing event existence check...");
// Load test configuration
let config_path = PathBuf::from("config-test-import.toml");
let config = Config::from_file(&config_path)?;
// Create CalDAV client for target server
let import_config = config.get_import_config().ok_or("No import configuration found")?;
let target_client = RealCalDavClient::new(
&import_config.target_server.url,
&import_config.target_server.username,
&import_config.target_server.password,
).await?;
// Build target calendar URL
let target_calendar_url = format!("{}/", import_config.target_server.url.trim_end_matches('/'));
// Test non-existent event
let fake_uid = "non-existent-event-67890";
let fake_event_url = format!("{}{}.ics", target_calendar_url, fake_uid);
println!("🔍 Testing existence check for non-existent event: {}", fake_uid);
let existence_result = target_client.check_event_exists(&fake_event_url).await;
match existence_result {
Ok(_) => {
println!("❌ Non-existent event reported as existing");
Err("Non-existent event reported as existing".into())
}
Err(e) => {
println!("✅ Non-existent event correctly reported as missing: {}", e);
Ok(())
}
}
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_library_initialization() -> CalDavResult<()> {
caldav_sync::init()?;
Ok(())
}
#[test]
fn test_version() {
assert!(!caldav_sync::VERSION.is_empty());
}
#[test]
fn test_full_workflow() -> CalDavResult<()> {
// Initialize library
caldav_sync::init()?;
// Create configuration
let config = Config::default();
// Validate configuration
config.validate()?;
// Create some test events
let event1 = caldav_sync::event::Event::new(
"Test Meeting".to_string(),
Utc::now(),
Utc::now() + chrono::Duration::hours(1),
);
let event2 = caldav_sync::event::Event::new_all_day(
"Test Holiday".to_string(),
chrono::NaiveDate::from_ymd_opt(2023, 12, 25).unwrap(),
);
// Test event serialization
let ical1 = event1.to_ical()?;
let ical2 = event2.to_ical()?;
assert!(!ical1.is_empty());
assert!(!ical2.is_empty());
assert!(ical1.contains("SUMMARY:Test Meeting"));
assert!(ical2.contains("SUMMARY:Test Holiday"));
// Test filtering
let filter = caldav_sync::calendar_filter::FilterBuilder::new()
.keywords(vec!["test".to_string()])
.build();
assert!(filter.matches_event(&event1));
assert!(filter.matches_event(&event2));
Ok(())
}
}