diff --git a/crates/caldav/src/calendar/methods/mkcalendar.rs b/crates/caldav/src/calendar/methods/mkcalendar.rs index 2d65e0c..0847a57 100644 --- a/crates/caldav/src/calendar/methods/mkcalendar.rs +++ b/crates/caldav/src/calendar/methods/mkcalendar.rs @@ -90,14 +90,14 @@ pub async fn route_mkcalendar( let calendar = IcalParser::new(tz.as_bytes()) .next() .ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))? - .map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?; + .map_err(|_| rustical_dav::Error::BadRequest("Error parsing timezone".to_owned()))?; let timezone = calendar.timezones.first().ok_or_else(|| { rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) })?; - let timezone: chrono_tz::Tz = timezone - .try_into() - .map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?; + let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| { + rustical_dav::Error::BadRequest("Cannot translate VTIMEZONE into IANA TZID".to_owned()) + })?; Some(timezone.name().to_owned()) } else { diff --git a/src/integration_tests/caldav/calendar.rs b/src/integration_tests/caldav/calendar.rs index b4478f2..475fe1d 100644 --- a/src/integration_tests/caldav/calendar.rs +++ b/src/integration_tests/caldav/calendar.rs @@ -29,12 +29,34 @@ fn mkcalendar_template( {displayname} {description} {color} - Europe/Berlin + @@ -209,3 +231,106 @@ async fn test_caldav_calendar( Err(rustical_store::Error::NotFound) )); } + +#[rstest] +#[tokio::test] +async fn test_rfc4791_5_3_2( + #[from(test_store_context)] + #[future] + context: TestStoreContext, +) { + let context = context.await; + let app = get_app(context.clone()); + + let calendar_meta = CalendarMetadata { + displayname: Some("Calendar".to_string()), + description: Some("Description".to_string()), + color: Some("#00FF00".to_string()), + order: 0, + }; + let (principal, cal_id) = ("user", "calendar"); + let url = format!("/caldav/principal/{principal}/{cal_id}"); + + let request_template = || { + Request::builder() + .method("MKCALENDAR") + .uri(&url) + .body(Body::from(mkcalendar_template(&calendar_meta))) + .unwrap() + }; + + // Try with correct credentials + let mut request = request_template(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + + let ical = r"BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:20010712T182145Z-123401@example.com +DTSTAMP:20060712T182145Z +DTSTART:20060714T170000Z +DTEND:20060715T040000Z +SUMMARY:Bastille Day Party +END:VEVENT +END:VCALENDAR"; + + let mut request = Request::builder() + .method("PUT") + .uri(format!("{url}/qwue23489.ics")) + .header("If-None-Match", "*") + .header("Content-Type", "text/calendar") + .body(Body::from(ical)) + .unwrap(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + + let mut request = Request::builder() + .method("PUT") + .uri(format!("{url}/qwue23489.ics")) + .header("If-None-Match", "*") + .header("Content-Type", "text/calendar") + .body(Body::from(ical)) + .unwrap(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::CONFLICT); + + let mut request = Request::builder() + .method("REPORT") + .uri(&url) + .header("Depth", "infinity") + .header("Content-Type", "text/xml; charset=\"utf-8\"") + .body(Body::from(format!( + r#" + + + + + + {url}/qwue23489.ics + /home/bernard/addressbook/vcf1.vcf + + "# + ))) + .unwrap(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::MULTI_STATUS); + let body = response.extract_string().await; + insta::assert_snapshot!("multiget_body", body); +} diff --git a/src/integration_tests/caldav/calendar_import.rs b/src/integration_tests/caldav/calendar_import.rs new file mode 100644 index 0000000..0a82060 --- /dev/null +++ b/src/integration_tests/caldav/calendar_import.rs @@ -0,0 +1,94 @@ +use crate::integration_tests::{ResponseExtractString, get_app}; +use axum::body::Body; +use axum::extract::Request; +use headers::{Authorization, HeaderMapExt}; +use http::StatusCode; +use rstest::rstest; +use rustical_store_sqlite::tests::{TestStoreContext, test_store_context}; +use tower::ServiceExt; + +const ICAL: &str = r" +BEGIN:VCALENDAR +PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 +BEGIN:VEVENT +UID:1@example.com +SUMMARY:One-off Meeting +DTSTAMP:20041210T183904Z +DTSTART:20041207T120000Z +DTEND:20041207T130000Z +END:VEVENT +BEGIN:VEVENT +UID:2@example.com +SUMMARY:Weekly Meeting +DTSTAMP:20041210T183838Z +DTSTART:20041206T120000Z +DTEND:20041206T130000Z +RRULE:FREQ=WEEKLY +END:VEVENT +BEGIN:VEVENT +UID:2@example.com +SUMMARY:Weekly Meeting +RECURRENCE-ID:20041213T120000Z +DTSTAMP:20041210T183838Z +DTSTART:20041213T130000Z +DTEND:20041213T140000Z +END:VEVENT +END:VCALENDAR +"; + +#[rstest] +#[tokio::test] +async fn test_import( + #[from(test_store_context)] + #[future] + context: TestStoreContext, +) { + let context = context.await; + let app = get_app(context.clone()); + + let (principal, addr_id) = ("user", "calendar"); + let url = format!("/caldav/principal/{principal}/{addr_id}"); + + let request_template = || { + Request::builder() + .method("IMPORT") + .uri(&url) + .body(Body::from(ICAL)) + .unwrap() + }; + + // Try without authentication + let request = request_template(); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + // Try with correct credentials + let mut request = request_template(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = response.extract_string().await; + insta::assert_snapshot!("import_body", body); + + let mut request = Request::builder() + .method("GET") + .uri(&url) + .body(Body::empty()) + .unwrap(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = response.extract_string().await; + insta::with_settings!({ + filters => vec![ + (r"UID:.+", "UID:[UID]") + ] + }, { + insta::assert_snapshot!("get_body", body); + }); +} diff --git a/src/integration_tests/caldav/mod.rs b/src/integration_tests/caldav/mod.rs index 101acc6..afb1024 100644 --- a/src/integration_tests/caldav/mod.rs +++ b/src/integration_tests/caldav/mod.rs @@ -8,6 +8,7 @@ use rustical_store_sqlite::tests::{TestStoreContext, test_store_context}; use tower::ServiceExt; mod calendar; +mod calendar_import; #[rstest] #[tokio::test] diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__get_body.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__get_body.snap index 84bf172..d0fc17b 100644 --- a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__get_body.snap +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__get_body.snap @@ -8,5 +8,5 @@ CALSCALE:GREGORIAN PRODID:RustiCal X-WR-CALNAME:Calendar X-WR-CALDESC:Description -X-WR-TIMEZONE:Europe/Berlin +X-WR-TIMEZONE:US/Eastern END:VCALENDAR diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__multiget_body.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__multiget_body.snap new file mode 100644 index 0000000..a247147 --- /dev/null +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__multiget_body.snap @@ -0,0 +1,20 @@ +--- +source: src/integration_tests/caldav/calendar.rs +expression: body +--- + + + + /caldav/principal/user/calendar/qwue23489.ics + + + "aea50382a7775bb9742bfec277382e3a260b6066f503b5f5ae34548d7215ee46" + + HTTP/1.1 200 OK + + + + /home/bernard/addressbook/vcf1.vcf + HTTP/1.1 404 Not Found + + diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__propfind_body.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__propfind_body.snap index fa54706..bc4c51b 100644 --- a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__propfind_body.snap +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar__propfind_body.snap @@ -14,109 +14,117 @@ expression: body PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN VERSION:2.0 BEGIN:VTIMEZONE -TZID:Europe/Berlin +TZID:America/New_York LAST-MODIFIED:20250723T190331Z -X-LIC-LOCATION:Europe/Berlin +X-LIC-LOCATION:America/New_York X-PROLEPTIC-TZNAME:LMT BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+005328 -TZOFFSETTO:+0100 -DTSTART:18930401T000000 +TZNAME:EST +TZOFFSETFROM:-045602 +TZOFFSETTO:-0500 +DTSTART:18831118T120358 END:STANDARD BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19160430T230000 -RDATE:19400401T020000 -RDATE:19430329T020000 -RDATE:19460414T020000 -RDATE:19470406T030000 -RDATE:19480418T020000 -RDATE:19490410T020000 -RDATE:19800406T020000 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +DTSTART:19180331T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T070000Z END:DAYLIGHT BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19161001T010000 -RDATE:19421102T030000 -RDATE:19431004T030000 -RDATE:19441002T030000 -RDATE:19451118T030000 -RDATE:19461007T030000 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +DTSTART:19181027T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19201031T060000Z END:STANDARD BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19170416T020000 -RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +DTSTART:19210424T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19410427T070000Z END:DAYLIGHT BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19170917T030000 -RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +DTSTART:19210925T020000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19410928T060000Z END:STANDARD BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19440403T020000 -RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z +TZNAME:EWT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +DTSTART:19420209T020000 END:DAYLIGHT BEGIN:DAYLIGHT -TZNAME:CEMT -TZOFFSETFROM:+0200 -TZOFFSETTO:+0300 -DTSTART:19450524T020000 -RDATE:19470511T030000 -END:DAYLIGHT -BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0300 -TZOFFSETTO:+0200 -DTSTART:19450924T030000 -RDATE:19470629T030000 +TZNAME:EPT +TZOFFSETFROM:-0400 +TZOFFSETTO:-0400 +DTSTART:19450814T190000 END:DAYLIGHT BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0100 -TZOFFSETTO:+0100 -DTSTART:19460101T000000 -RDATE:19800101T000000 -END:STANDARD -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19471005T030000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z -END:STANDARD -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19800928T030000 -RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +DTSTART:19450930T020000 END:STANDARD BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19810329T020000 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +DTSTART:19460428T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z END:DAYLIGHT BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19961027T030000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +DTSTART:19460929T020000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19540926T060000Z +END:STANDARD +BEGIN:STANDARD +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +DTSTART:19551030T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +DTSTART:19740106T020000 +RDATE:19750223T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +DTSTART:19760425T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z +END:DAYLIGHT +BEGIN:DAYLIGHT +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z +END:DAYLIGHT +BEGIN:DAYLIGHT +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +DTSTART:20070311T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +DTSTART:20071104T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU END:STANDARD END:VTIMEZONE END:VCALENDAR @@ -124,7 +132,7 @@ END:VCALENDAR https://www.iana.org/time-zones - Europe/Berlin + US/Eastern 0 @@ -139,7 +147,7 @@ END:VCALENDAR i;unicode-casemap i;octet - 10000000 + 10000000 diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__get_body.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__get_body.snap new file mode 100644 index 0000000..7dc2838 --- /dev/null +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__get_body.snap @@ -0,0 +1,32 @@ +--- +source: src/integration_tests/caldav/calendar_import.rs +expression: body +--- +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:RustiCal +BEGIN:VEVENT +UID:[UID] +SUMMARY:One-off Meeting +DTSTAMP:20041210T183904Z +DTSTART:20041207T120000Z +DTEND:20041207T130000Z +END:VEVENT +BEGIN:VEVENT +UID:[UID] +SUMMARY:Weekly Meeting +DTSTAMP:20041210T183838Z +DTSTART:20041206T120000Z +DTEND:20041206T130000Z +RRULE:FREQ=WEEKLY +END:VEVENT +BEGIN:VEVENT +UID:[UID] +SUMMARY:Weekly Meeting +RECURRENCE-ID:20041213T120000Z +DTSTAMP:20041210T183838Z +DTSTART:20041213T130000Z +DTEND:20041213T140000Z +END:VEVENT +END:VCALENDAR diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__import_body.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__import_body.snap new file mode 100644 index 0000000..5d87409 --- /dev/null +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__import_body.snap @@ -0,0 +1,5 @@ +--- +source: src/integration_tests/caldav/calendar_import.rs +expression: body +--- +