From 44ae995f2954f1706f6a4fce55b5928de5cfdf31 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Wed, 31 Dec 2025 19:54:06 +0100 Subject: [PATCH] Some small fixes on recurrence expansion --- Cargo.lock | 2 + crates/ical/Cargo.toml | 2 + crates/ical/src/icalendar/event.rs | 185 ++++++++++--- crates/ical/src/timestamp.rs | 8 + crates/xml/src/unparsed.rs | 9 +- docs/developers/rfcs/rfc4791.md | 253 ++++++++++++++++++ .../caldav/calendar_report.rs | 196 ++++++++++++++ src/integration_tests/caldav/mod.rs | 1 + ...__caldav__calendar_import__1_get_body.snap | 16 +- ...aldav__calendar_report__0_report_body.snap | 100 +++++++ ...aldav__calendar_report__1_report_body.snap | 98 +++++++ ...v__calendar_report__2_report_body.snap.new | 63 +++++ 12 files changed, 887 insertions(+), 46 deletions(-) create mode 100644 docs/developers/rfcs/rfc4791.md create mode 100644 src/integration_tests/caldav/calendar_report.rs create mode 100644 src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__0_report_body.snap create mode 100644 src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__1_report_body.snap create mode 100644 src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__2_report_body.snap.new diff --git a/Cargo.lock b/Cargo.lock index ab0ad63..b6e3178 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3490,9 +3490,11 @@ dependencies = [ "ical", "regex", "rrule", + "rstest", "rustical_xml", "serde", "sha2", + "similar-asserts", "thiserror 2.0.17", ] diff --git a/crates/ical/Cargo.toml b/crates/ical/Cargo.toml index 8318de8..3a14828 100644 --- a/crates/ical/Cargo.toml +++ b/crates/ical/Cargo.toml @@ -21,3 +21,5 @@ rrule.workspace = true serde.workspace = true sha2.workspace = true axum.workspace = true +rstest.workspace = true +similar-asserts.workspace = true diff --git a/crates/ical/src/icalendar/event.rs b/crates/ical/src/icalendar/event.rs index 9449554..14d3a67 100644 --- a/crates/ical/src/icalendar/event.rs +++ b/crates/ical/src/icalendar/event.rs @@ -67,6 +67,8 @@ impl EventObject { }; let mut rrule_set = RRuleSet::new(dtstart); + // TODO: Make nice, this is just a bodge to get correct behaviour + let mut empty = true; for prop in &self.event.properties { rrule_set = match prop.name.as_str() { @@ -76,49 +78,63 @@ impl EventObject { })?)? .validate(dtstart) .unwrap(); + empty = false; rrule_set.rrule(rrule) } "RDATE" => { let rdate = CalDateTime::parse_prop(prop, &self.timezones)?.into(); + empty = false; rrule_set.rdate(rdate) } "EXDATE" => { let exdate = CalDateTime::parse_prop(prop, &self.timezones)?.into(); + empty = false; rrule_set.exdate(exdate) } _ => rrule_set, } } + if empty { + return Ok(None); + } Ok(Some(rrule_set)) } + // The returned calendar components MUST NOT use recurrence + // properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT + // have reference to or include VTIMEZONE components. Date and local + // time with reference to time zone information MUST be converted + // into date with UTC time. pub fn expand_recurrence( &self, start: Option>, end: Option>, overrides: &[Self], ) -> Result, Error> { - let Some(mut rrule_set) = self.recurrence_ruleset()? else { - return Ok(vec![self.event.clone()]); - }; + let mut events = vec![]; + let dtstart = self.get_dtstart()?.expect("We must have a DTSTART here"); + let computed_duration = self + .get_dtend()? + .map(|dtend| dtend.as_datetime().into_owned() - dtstart.as_datetime().as_ref()); + let Some(mut rrule_set) = self.recurrence_ruleset()? else { + // If ruleset empty simply return main event AND all overrides + return Ok(std::iter::once(self.clone()) + .chain(overrides.iter().cloned()) + .map(|event| event.event) + .collect()); + }; if let Some(start) = start { rrule_set = rrule_set.after(start.with_timezone(&rrule::Tz::UTC)); } if let Some(end) = end { rrule_set = rrule_set.before(end.with_timezone(&rrule::Tz::UTC)); } - let mut events = vec![]; let dates = rrule_set.all(2048).dates; - let dtstart = self.get_dtstart()?.expect("We must have a DTSTART here"); - let computed_duration = self - .get_dtend()? - .map(|dtend| dtend.as_datetime().into_owned() - dtstart.as_datetime().as_ref()); - 'recurrence: for date in dates { - let date = CalDateTime::from(date); - let dateformat = if dtstart.is_date() { + let date = CalDateTime::from(date.to_utc()); + let recurrence_id = if dtstart.is_date() { date.format_date() } else { date.format() @@ -131,7 +147,7 @@ impl EventObject { .as_ref() .expect("overrides have a recurrence id") .value - && override_id == &dateformat + && override_id == &recurrence_id { // We have an override for this occurence // @@ -154,13 +170,13 @@ impl EventObject { ev.set_property(Property { name: "RECURRENCE-ID".to_string(), - value: Some(dateformat.clone()), + value: Some(recurrence_id.clone()), params: vec![], }); ev.set_property(Property { name: "DTSTART".to_string(), - value: Some(dateformat), - params: dtstart_prop.params.clone(), + value: Some(recurrence_id), + params: vec![], }); if let Some(duration) = computed_duration { let dtend = date + duration; @@ -183,10 +199,12 @@ impl EventObject { #[cfg(test)] mod tests { - use crate::CalendarObject; + use crate::{CalDateTime, CalendarObject}; + use chrono::{DateTime, Utc}; use ical::generator::Emitter; + use rstest::rstest; - const ICS: &str = r"BEGIN:VCALENDAR + const ICS_1: &str = r"BEGIN:VCALENDAR CALSCALE:GREGORIAN VERSION:2.0 BEGIN:VTIMEZONE @@ -206,16 +224,16 @@ RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU END:VEVENT END:VCALENDAR"; - const EXPANDED: [&str; 4] = [ + const EXPANDED_1: &[&str] = &[ "BEGIN:VEVENT\r UID:318ec6503573d9576818daf93dac07317058d95c\r DTSTAMP:20250502T132758Z\r SEQUENCE:2\r SUMMARY:weekly stuff\r TRANSP:OPAQUE\r -RECURRENCE-ID:20250506T090000\r -DTSTART;TZID=Europe/Berlin:20250506T090000\r -DTEND;TZID=Europe/Berlin:20250506T092500\r +RECURRENCE-ID:20250506T070000Z\r +DTSTART:20250506T070000Z\r +DTEND:20250506T072500Z\r END:VEVENT\r\n", "BEGIN:VEVENT\r UID:318ec6503573d9576818daf93dac07317058d95c\r @@ -223,9 +241,9 @@ DTSTAMP:20250502T132758Z\r SEQUENCE:2\r SUMMARY:weekly stuff\r TRANSP:OPAQUE\r -RECURRENCE-ID:20250508T090000\r -DTSTART;TZID=Europe/Berlin:20250508T090000\r -DTEND;TZID=Europe/Berlin:20250508T092500\r +RECURRENCE-ID:20250508T070000Z\r +DTSTART:20250508T070000Z\r +DTEND:20250508T072500Z\r END:VEVENT\r\n", "BEGIN:VEVENT\r UID:318ec6503573d9576818daf93dac07317058d95c\r @@ -234,8 +252,8 @@ SEQUENCE:2\r SUMMARY:weekly stuff\r TRANSP:OPAQUE\r RECURRENCE-ID:20250511T090000\r -DTSTART;TZID=Europe/Berlin:20250511T090000\r -DTEND;TZID=Europe/Berlin:20250511T092500\r +DTSTART:20250511T070000Z\r +DTEND:20250511T072500Z\r END:VEVENT\r\n", "BEGIN:VEVENT\r UID:318ec6503573d9576818daf93dac07317058d95c\r @@ -244,25 +262,124 @@ SEQUENCE:2\r SUMMARY:weekly stuff\r TRANSP:OPAQUE\r RECURRENCE-ID:20250520T090000\r -DTSTART;TZID=Europe/Berlin:20250520T090000\r -DTEND;TZID=Europe/Berlin:20250520T092500\r +DTSTA:20250520T070000Z\r +DTEND:20250520T072500Z\r END:VEVENT\r\n", ]; - #[test] - fn test_expand_recurrence() { - let event = CalendarObject::from_ics(ICS.to_string(), None).unwrap(); + const ICS_2: &str = r"BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:US/Eastern +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DTSTART;TZID=US/Eastern:20060102T120000 +DURATION:PT1H +RRULE:FREQ=DAILY;COUNT=5 +SUMMARY:Event #2 +UID:abcd2 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DTSTART;TZID=US/Eastern:20060104T140000 +DURATION:PT1H +RECURRENCE-ID;TZID=US/Eastern:20060104T120000 +SUMMARY:Event #2 bis +UID:abcd2 +END:VEVENT +END:VCALENDAR +"; + + const EXPANDED_2: &[&str] = &[ + "BEGIN:VEVENT\r +DTSTAMP:20060206T001121Z\r +DURATION:PT1H\r +SUMMARY:Event #2\r +UID:abcd2\r +RECURRENCE-ID:20060103T170000\r +DTSTART:20060103T170000\r +END:VEVENT\r\n", + "BEGIN:VEVENT\r +DTSTAMP:20060206T001121Z\r +DURATION:PT1H\r +SUMMARY:Event #2 bis\r +UID:abcd2\r +RECURRENCE-ID:20060104T170000\r +DTSTART:20060104T190000\r +END:VEVENT\r +END:VCALENDAR\r\n", + ]; + + const ICS_3: &str = r"BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:US/Eastern +END:VTIMEZONE +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com +DTSTAMP:20060206T001220Z +DTSTART;TZID=US/Eastern:20060104T100000 +DURATION:PT1H +LAST-MODIFIED:20060206T001330Z +ORGANIZER:mailto:cyrus@example.com +SEQUENCE:1 +STATUS:TENTATIVE +SUMMARY:Event #3 +UID:abcd3 +END:VEVENT +END:VCALENDAR +"; + + const EXPANDED_3: &[&str] = &["BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com +DTSTAMP:20060206T001220Z +DTSTART:20060104T150000 +DURATION:PT1H +LAST-MODIFIED:20060206T001330Z +ORGANIZER:mailto:cyrus@example.com +SEQUENCE:1 +STATUS:TENTATIVE +SUMMARY:Event #3 +UID:abcd3 +X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com +END:VEVENT"]; + + #[rstest] + #[case(ICS_1, EXPANDED_1, None, None)] + // from https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.3 + #[case(ICS_2, EXPANDED_2, + Some(CalDateTime::parse("20060103T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc()), + Some(CalDateTime::parse("20060105T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc()) + )] + #[case(ICS_3, EXPANDED_3, + Some(CalDateTime::parse("20060103T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc()), + Some(CalDateTime::parse("20060105T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc()) + )] + fn test_expand_recurrence( + #[case] ics: &'static str, + #[case] expanded: &[&str], + #[case] from: Option>, + #[case] to: Option>, + ) { + let event = CalendarObject::from_ics(ics.to_string(), None).unwrap(); let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else { panic!() }; let events: Vec = event - .expand_recurrence(None, None, overrides) + .expand_recurrence(from, to, overrides) .unwrap() .into_iter() .map(|event| Emitter::generate(&event)) .collect(); - assert_eq!(events.as_slice()[0], EXPANDED[0]); - assert_eq!(events.as_slice(), &EXPANDED); + assert_eq!(events.len(), expanded.len()); + for (output, reference) in events.iter().zip(expanded) { + similar_asserts::assert_eq!(output, reference); + } } } diff --git a/crates/ical/src/timestamp.rs b/crates/ical/src/timestamp.rs index 623bc26..2dcf037 100644 --- a/crates/ical/src/timestamp.rs +++ b/crates/ical/src/timestamp.rs @@ -198,6 +198,14 @@ impl CalDateTime { } } + #[must_use] + pub fn with_timezone(&self, tz: &ICalTimezone) -> Self { + match self { + Self::DateTime(datetime) => Self::DateTime(datetime.with_timezone(tz)), + Self::Date(date, _) => Self::Date(date.to_owned(), tz.to_owned()), + } + } + pub fn parse(value: &str, timezone: Option) -> Result { if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) { if let Some(timezone) = timezone { diff --git a/crates/xml/src/unparsed.rs b/crates/xml/src/unparsed.rs index 1a2cdcd..f8873a1 100644 --- a/crates/xml/src/unparsed.rs +++ b/crates/xml/src/unparsed.rs @@ -5,14 +5,14 @@ use quick_xml::events::BytesStart; use crate::{XmlDeserialize, XmlError}; // TODO: actually implement -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Unparsed(BytesStart<'static>); +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Unparsed(String); impl Unparsed { #[must_use] pub fn tag_name(&self) -> String { // TODO: respect namespace? - String::from_utf8_lossy(self.0.local_name().as_ref()).to_string() + self.0.clone() } } @@ -27,6 +27,7 @@ impl XmlDeserialize for Unparsed { let mut buf = vec![]; reader.read_to_end_into(start.name(), &mut buf)?; } - Ok(Self(start.to_owned())) + let tag_name = String::from_utf8_lossy(start.local_name().as_ref()).to_string(); + Ok(Self(tag_name)) } } diff --git a/docs/developers/rfcs/rfc4791.md b/docs/developers/rfcs/rfc4791.md new file mode 100644 index 0000000..f85e2a2 --- /dev/null +++ b/docs/developers/rfcs/rfc4791.md @@ -0,0 +1,253 @@ +# RFC 4791 (CalDAV) + +## ☑ 1. Introduction + +### ☑ 1.1 Notational Conventions + +### ☑ 1.2 XML Namespaces and Processing + +### ☐ 1.3 Method Preconditions and Postconditions + +## ☐ 2. Requirements Overview + +- [x] MUST support iCalendar [RFC2445] as a media type for the calendar + object resource format; + +- [ ] MUST support WebDAV Class 1 [RFC2518] (note that [rfc2518bis] + describes clarifications to [RFC2518] that aid interoperability); + +- [x] MUST support WebDAV ACL [RFC3744] with the additional privilege + defined in Section 6.1 of this document; + +- [x] MUST support transport over TLS [RFC2246] as defined in [RFC2818] + (note that [RFC2246] has been obsoleted by [RFC4346]); + +- [x] MUST support ETags [RFC2616] with additional requirements + specified in Section 5.3.4 of this document; + +- [ ] MUST support all calendaring reports defined in Section 7 of this + document; and + +- [x] MUST advertise support on all calendar collections and calendar + object resources for the calendaring reports in the DAV:supported- + report-set property, as defined in Versioning Extensions to WebDAV + [RFC3253]. + + In addition, a server: + +- [x] SHOULD support the MKCALENDAR method defined in Section 5.3.1 of + this document. + +## ☑ 3. Calendaring Data Model + +### ☑ 3.1 Calendar Server + +### ☑ 3.2 Recurrence and the Data Model + +## ☑ 4. Calendar Resources + +### ☑ 4.1 Calendar Object Resources + +### ☑ 4.2 Calendar Collection + +## ☐ 5. Calendar Access Feature + +### ☑ 5.1 Calendar Access Support + +#### ☑ 5.1.1 Example: Using OPTIONS for the Discovery of Calendar Access Support + +### ☑ 5.2 Calendar Collection Properties + +#### ☑ 5.2.1 CALDAV:calendar-description Property + +#### ☑ 5.2.2 CALDAV:calendar-timezone Property + +#### ☑ 5.2.3 CALDAV:supported-calendar-component-set Property + +#### ☑ 5.2.4 CALDAV:supported-calendar-data Property + +#### ☑ 5.2.5 CALDAV:max-resource-size Property + +#### ☑ 5.2.6 CALDAV:min-date-time Property + +#### ☑ 5.2.7 CALDAV:max-date-time Property + +#### ☐ 5.2.8 CALDAV:max-instances Property (Maybe set this :)) + +#### ☑ 5.2.9 CALDAV:max-attendees-per-instance Property (does not apply) + +#### ☑ 5.2.10 Additional Precondition for PROPPATCH + +### ☑ 5.3 Creating Resources + +#### ☑ 5.3.1 MKCALENDAR Method + +##### ☑ 5.3.1.1 Status Codes + +##### ☑ 5.3.1.2 Example: Successful MKCALENDAR Request + +- Example fails because of the tzid is not in the Olson database, but that's okay + +#### ☑ 5.3.2 Creating Calendar Object Resources + +##### ☐ 5.3.2.1 Additional Preconditions for PUT, COPY, and MOVE + +### ☑ 5.3.3 Non-Standard Components, Properties, and Parameters + +### ☑ 5.3.4 Calendar Object Resource Entity Tag + +## ☐ 6. Calendaring Access Control + +### ☐ 6.1 Calendaring Privilege + +#### ☐ 6.1.1 CALDAV:read-free-busy Privilege + +### ☑ 6.2 Additional Principal Property + +#### ☑ 6.2.1 CALDAV:calendar-home-set Property + +## ☐ 7. Calendaring Reports + +- [ ] `DAV:expand-property` + +### ☑ 7.1 REPORT Method + +### ☑ 7.2 Ordinary Collections + +### ☑ 7.3 Date and Floating Time + +### ☑ 7.4 Time Range Filtering + +### ☑ 7.5 Searching Text: Collations + +#### ☑ 7.5.1 CALDAV:supported-collation-set Property + +### ☐ 7.6 Partial Retrieval + +### ☑ 7.7 Non-Standard Components, Properties, and Parameters + +### ☑ 7.8 CALDAV:calendar-query REPORT + +#### ☐ 7.8.1 Example: Partial Retrieval of Events by Time Range + +#### ☐ 7.8.2 Example: Partial Retrieval of Recurring Events + +#### ☐ 7.8.3 Example: Expanded Retrieval of Recurring Events + +#### ☐ 7.8.4 Example: Partial Retrieval of Stored Free Busy Components + +#### ☐ 7.8.5 Example: Retrieval of To-Dos by Alarm Time Range + +#### ☐ 7.8.6 Example: Retrieval of Event by UID + +#### ☐ 7.8.7 Example: Retrieval of Events by PARTSTAT + +#### ☐ 7.8.8 Example: Retrieval of Events Only + +#### ☐ 7.8.9 Example: Retrieval of All Pending To-Dos + +#### ☐ 7.8.10 Example: Attempt to Query Unsupported Property + +### ☐ 7.9 CALDAV:calendar-multiget REPORT + +#### ☐ 7.9.1 Example: Successful CALDAV:calendar-multiget REPORT + +### ☐ 7.10 CALDAV:free-busy-query REPORT + +#### ☐ 7.10.1 Example: Successful CALDAV:free-busy-query REPORT + +## ☐ 8. Guidelines + +### ☐ 8.1 Client-to-Client Interoperability + +### ☐ 8.2 Synchronization Operations + +#### ☐ 8.2.1 Use of Reports + +##### ☐ 8.2.1.1 Restrict the Time Range + +##### ☐ 8.2.1.2 Synchronize by Time Range + +##### ☐ 8.2.1.3 Synchronization Process + +#### ☐ 8.2.2 Restrict the Properties Returned + +### ☐ 8.3 Use of Locking + +### ☐ 8.4 Finding Calendars + +### ☐ 8.5 Storing and Using Attachments + +#### ☐ 8.5.1 Inline Attachments + +#### ☐ 8.5.2 External Attachments + +### ☐ 8.6 Storing and Using Alarms + +## ☐ 9. XML Element Definitions + +### ☐ 9.1 CALDAV:calendar XML Element + +### ☐ 9.2 CALDAV:mkcalendar XML Element + +### ☐ 9.3 CALDAV:mkcalendar-response XML Element + +### ☐ 9.4 CALDAV:supported-collation XML Element + +### ☐ 9.5 CALDAV:calendar-query XML Element + +### ☐ 9.6 CALDAV:calendar-data XML Element + +#### ☐ 9.6.1 CALDAV:comp XML Element + +#### ☐ 9.6.2 CALDAV:allcomp XML Element + +#### ☐ 9.6.3 CALDAV:allprop XML Element + +#### ☐ 9.6.4 CALDAV:prop XML Element + +#### ☐ 9.6.5 CALDAV:expand XML Element + +#### ☐ 9.6.6 CALDAV:limit-recurrence-set XML Element + +#### ☐ 9.6.7 CALDAV:limit-freebusy-set XML Element + +### ☐ 9.7 CALDAV:filter XML Element + +#### ☐ 9.7.1 CALDAV:comp-filter XML Element + +#### ☐ 9.7.2 CALDAV:prop-filter XML Element + +#### ☐ 9.7.3 CALDAV:param-filter XML Element + +#### ☐ 9.7.4 CALDAV:is-not-defined XML Element + +#### ☐ 9.7.5 CALDAV:text-match XML Element + +### ☐ 9.8 CALDAV:timezone XML Element + +### ☐ 9.9 CALDAV:time-range XML Element + +### ☐ 9.10 CALDAV:calendar-multiget XML Element + +### ☐ 9.11 CALDAV:free-busy-query XML Element + +## ☐ 10. Internationalization Considerations + +## ☐ 11. Security Considerations + +## ☐ 12. IANA Considerations + +### ☐ 12.1 Namespace Registration + +## ☐ 13. Acknowledgements + +## ☐ 14. References + +### ☐ 14.1 Normative References + +### ☐ 14.2 Informative References + +## ☐ A. CalDAV Method Privilege Table (Normative) + +## ☐ B. Calendar Collections Used in the Examples diff --git a/src/integration_tests/caldav/calendar_report.rs b/src/integration_tests/caldav/calendar_report.rs new file mode 100644 index 0000000..879e6f3 --- /dev/null +++ b/src/integration_tests/caldav/calendar_report.rs @@ -0,0 +1,196 @@ +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 ICS_1: &str = include_str!("resources/rfc4791_appb.ics"); + +const REPORT_7_8_1: &str = r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#; + +const REPORT_7_8_2: &str = r#" + + + + + + + + + + + + + + + +"#; + +const REPORT_7_8_3: &str = r#" + + + + + + + + + + + + + + + +"#; + +const OUTPUT_7_8_3: &str = r#" + + http://cal.example.com/bernard/work/abcd2.ics + + + "fffff-abcd2" + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:-//Example Corp.//CalDAV Client//EN + BEGIN:VEVENT + DTSTAMP:20060206T001121Z + DTSTART:20060103T170000 + DURATION:PT1H + RECURRENCE-ID:20060103T170000 + SUMMARY:Event #2 + UID:00959BC664CA650E933C892C@example.com + END:VEVENT + BEGIN:VEVENT + DTSTAMP:20060206T001121Z + DTSTART:20060104T190000 + DURATION:PT1H + RECURRENCE-ID:20060104T170000 + SUMMARY:Event #2 bis + UID:00959BC664CA650E933C892C@example.com + END:VEVENT + END:VCALENDAR + + + HTTP/1.1 200 OK + + + + http://cal.example.com/bernard/work/abcd3.ics + + + "fffff-abcd3" + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:-//Example Corp.//CalDAV Client//EN + BEGIN:VEVENT + ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com + ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com + DTSTAMP:20060206T001220Z + DTSTART:20060104T150000 + DURATION:PT1H + LAST-MODIFIED:20060206T001330Z + ORGANIZER:mailto:cyrus@example.com + SEQUENCE:1 + STATUS:TENTATIVE + SUMMARY:Event #3 + UID:DC6C50A017428C5216A2F1CD@example.com + X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com + END:VEVENT + END:VCALENDAR + + + HTTP/1.1 200 OK + +"#; + +#[rstest] +#[case(0, ICS_1, REPORT_7_8_1)] +#[case(1, ICS_1, REPORT_7_8_2)] +#[case(2, ICS_1, REPORT_7_8_3)] +#[tokio::test] +async fn test_report( + #[from(test_store_context)] + #[future] + context: TestStoreContext, + #[case] case: usize, + #[case] ics: &'static str, + #[case] report: &'static str, +) { + 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(ics)) + .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::OK); + + let mut request = Request::builder() + .method("REPORT") + .uri(&url) + .body(Body::from(report)) + .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!(format!("{case}_report_body"), body); +} diff --git a/src/integration_tests/caldav/mod.rs b/src/integration_tests/caldav/mod.rs index afb1024..4a91cd7 100644 --- a/src/integration_tests/caldav/mod.rs +++ b/src/integration_tests/caldav/mod.rs @@ -9,6 +9,7 @@ use tower::ServiceExt; mod calendar; mod calendar_import; +mod calendar_report; #[rstest] #[tokio::test] diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__1_get_body.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__1_get_body.snap index 9f34fbb..8b15580 100644 --- a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__1_get_body.snap +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_import__1_get_body.snap @@ -25,14 +25,6 @@ TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE BEGIN:VEVENT -DTSTAMP:20060206T001121Z -DTSTART;TZID=US/Eastern:20060104T140000 -DURATION:PT1H -RECURRENCE-ID;TZID=US/Eastern:20060104T120000 -SUMMARY:Event #2 bis -UID:[UID] -END:VEVENT -BEGIN:VEVENT DTSTAMP:20060206T001102Z DTSTART;TZID=US/Eastern:20060102T100000 DURATION:PT1H @@ -49,6 +41,14 @@ SUMMARY:Event #2 UID:[UID] END:VEVENT BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DTSTART;TZID=US/Eastern:20060104T140000 +DURATION:PT1H +RECURRENCE-ID;TZID=US/Eastern:20060104T120000 +SUMMARY:Event #2 bis +UID:[UID] +END:VEVENT +BEGIN:VEVENT ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com DTSTAMP:20060206T001220Z diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__0_report_body.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__0_report_body.snap new file mode 100644 index 0000000..b179127 --- /dev/null +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__0_report_body.snap @@ -0,0 +1,100 @@ +--- +source: src/integration_tests/caldav/calendar_report.rs +expression: body +--- + + + + /caldav/principal/user/calendar/abcd2.ics + + + "7d80077c5655339885a36b6dbe97336767fb85e6b12c94668bcac100ed971fac" + BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DTSTART;TZID=US/Eastern:20060102T120000 +DURATION:PT1H +RRULE:FREQ=DAILY;COUNT=5 +SUMMARY:Event #2 +UID:abcd2 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DTSTART;TZID=US/Eastern:20060104T140000 +DURATION:PT1H +RECURRENCE-ID;TZID=US/Eastern:20060104T120000 +SUMMARY:Event #2 bis +UID:abcd2 +END:VEVENT +END:VCALENDAR + + + HTTP/1.1 200 OK + + + + /caldav/principal/user/calendar/abcd3.ics + + + "c6a5b1cf6985805686df99e7f2e1cf286567dcb3383fc6fa1b12ce42d3fbc01c" + BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com +DTSTAMP:20060206T001220Z +DTSTART;TZID=US/Eastern:20060104T100000 +DURATION:PT1H +LAST-MODIFIED:20060206T001330Z +ORGANIZER:mailto:cyrus@example.com +SEQUENCE:1 +STATUS:TENTATIVE +SUMMARY:Event #3 +UID:abcd3 +END:VEVENT +END:VCALENDAR + + + HTTP/1.1 200 OK + + + diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__1_report_body.snap b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__1_report_body.snap new file mode 100644 index 0000000..f3e413b --- /dev/null +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__1_report_body.snap @@ -0,0 +1,98 @@ +--- +source: src/integration_tests/caldav/calendar_report.rs +expression: body +--- + + + + /caldav/principal/user/calendar/abcd2.ics + + + BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DTSTART;TZID=US/Eastern:20060102T120000 +DURATION:PT1H +RRULE:FREQ=DAILY;COUNT=5 +SUMMARY:Event #2 +UID:abcd2 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DTSTART;TZID=US/Eastern:20060104T140000 +DURATION:PT1H +RECURRENCE-ID;TZID=US/Eastern:20060104T120000 +SUMMARY:Event #2 bis +UID:abcd2 +END:VEVENT +END:VCALENDAR + + + HTTP/1.1 200 OK + + + + /caldav/principal/user/calendar/abcd3.ics + + + BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com +DTSTAMP:20060206T001220Z +DTSTART;TZID=US/Eastern:20060104T100000 +DURATION:PT1H +LAST-MODIFIED:20060206T001330Z +ORGANIZER:mailto:cyrus@example.com +SEQUENCE:1 +STATUS:TENTATIVE +SUMMARY:Event #3 +UID:abcd3 +END:VEVENT +END:VCALENDAR + + + HTTP/1.1 200 OK + + + diff --git a/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__2_report_body.snap.new b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__2_report_body.snap.new new file mode 100644 index 0000000..17720ef --- /dev/null +++ b/src/integration_tests/caldav/snapshots/rustical__integration_tests__caldav__calendar_report__2_report_body.snap.new @@ -0,0 +1,63 @@ +--- +source: src/integration_tests/caldav/calendar_report.rs +assertion_line: 195 +expression: body +--- + + + + /caldav/principal/user/calendar/abcd2.ics + + + BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DURATION:PT1H +SUMMARY:Event #2 +UID:abcd2 +RECURRENCE-ID:20060103T170000Z +DTSTART:20060103T170000Z +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20060206T001121Z +DURATION:PT1H +SUMMARY:Event #2 +UID:abcd2 +RECURRENCE-ID:20060104T170000Z +DTSTART:20060104T170000Z +END:VEVENT +END:VCALENDAR + + + HTTP/1.1 200 OK + + + + /caldav/principal/user/calendar/abcd3.ics + + + BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com +DTSTAMP:20060206T001220Z +DTSTART;TZID=US/Eastern:20060104T100000 +DURATION:PT1H +LAST-MODIFIED:20060206T001330Z +ORGANIZER:mailto:cyrus@example.com +SEQUENCE:1 +STATUS:TENTATIVE +SUMMARY:Event #3 +UID:abcd3 +END:VEVENT +END:VCALENDAR + + + HTTP/1.1 200 OK + + +