diff --git a/Cargo.lock b/Cargo.lock index 3b8adf6..d4b0ee6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3093,6 +3093,7 @@ dependencies = [ "rustical_store_sqlite", "rustical_xml", "serde", + "serde_json", "sha2", "strum", "strum_macros", diff --git a/crates/caldav/Cargo.toml b/crates/caldav/Cargo.toml index 2270f5a..2903282 100644 --- a/crates/caldav/Cargo.toml +++ b/crates/caldav/Cargo.toml @@ -11,6 +11,7 @@ publish = false rustical_store_sqlite = { workspace = true, features = ["test"] } rstest.workspace = true async-std.workspace = true +serde_json.workspace = true [dependencies] axum.workspace = true diff --git a/crates/caldav/src/calendar/mod.rs b/crates/caldav/src/calendar/mod.rs index 2d092d8..d8df251 100644 --- a/crates/caldav/src/calendar/mod.rs +++ b/crates/caldav/src/calendar/mod.rs @@ -4,3 +4,6 @@ pub mod resource; mod service; pub use service::CalendarResourceService; + +#[cfg(test)] +pub mod tests; diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index 37b7c57..9afdbe0 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -16,6 +16,7 @@ use rustical_store::Calendar; use rustical_store::auth::Principal; use rustical_xml::{EnumVariants, PropName}; use rustical_xml::{XmlDeserialize, XmlSerialize}; +use serde::Deserialize; #[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[xml(unit_variants_ident = "CalendarPropName")] @@ -62,7 +63,7 @@ pub enum CalendarPropWrapper { Common(CommonPropertiesProp), } -#[derive(Clone, Debug, From, Into)] +#[derive(Clone, Debug, From, Into, Deserialize)] pub struct CalendarResource { pub cal: Calendar, pub read_only: bool, diff --git a/crates/caldav/src/calendar/test_files/propfind.outputs b/crates/caldav/src/calendar/test_files/propfind.outputs new file mode 100644 index 0000000..38f4e04 --- /dev/null +++ b/crates/caldav/src/calendar/test_files/propfind.outputs @@ -0,0 +1,222 @@ + + + /caldav/principal/user/calendar/ + + + + + + + + + + + + + + + + + + + + + + + + + + + HTTP/1.1 200 OK + + + + + + + /caldav/principal/user/calendar/ + + + BEGIN:VCALENDAR +PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +LAST-MODIFIED:20250723T190331Z +X-LIC-LOCATION:Europe/Berlin +X-PROLEPTIC-TZNAME:LMT +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+005328 +TZOFFSETTO:+0100 +DTSTART:18930401T000000 +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 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19161001T010000 +RDATE:19421102T030000 +RDATE:19431004T030000 +RDATE:19441002T030000 +RDATE:19451118T030000 +RDATE:19461007T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19170416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19170917T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19440403T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z +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 +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 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19810329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19961027T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +END:VCALENDAR + + + https://www.iana.org/time-zones + + Europe/Berlin + 0 + + + + + + + + 10000000 + + + + + + + + + + + + + + + + + + -2621430101T000000Z + +2621421231T235959Z + github.com/lennart-k/rustical/ns/12 + github.com/lennart-k/rustical/ns/12 + + + + b28b41e9-8801-4fc5-ae29-8efb5fadeb36 + + + 1 + + + 1 + + + + + + + Calendar + + /caldav/principal/user/ + + + + + + + + + + + + + + /caldav/principal/user/ + + + HTTP/1.1 200 OK + + diff --git a/crates/caldav/src/calendar/test_files/propfind.principals.json b/crates/caldav/src/calendar/test_files/propfind.principals.json new file mode 100644 index 0000000..ec1270d --- /dev/null +++ b/crates/caldav/src/calendar/test_files/propfind.principals.json @@ -0,0 +1,11 @@ +[ + { + "id": "user", + "displayname": null, + "principal_type": "individual", + "password": null, + "memberships": [ + "group" + ] + } +] diff --git a/crates/caldav/src/calendar/test_files/propfind.requests b/crates/caldav/src/calendar/test_files/propfind.requests new file mode 100644 index 0000000..846c969 --- /dev/null +++ b/crates/caldav/src/calendar/test_files/propfind.requests @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/caldav/src/calendar/test_files/propfind.resources.json b/crates/caldav/src/calendar/test_files/propfind.resources.json new file mode 100644 index 0000000..e042f61 --- /dev/null +++ b/crates/caldav/src/calendar/test_files/propfind.resources.json @@ -0,0 +1,42 @@ +[ + { + "cal": { + "principal": "user", + "id": "calendar", + "displayname": "Calendar", + "order": 0, + "description": null, + "color": null, + "timezone_id": "Europe/Berlin", + "deleted_at": null, + "synctoken": 12, + "subscription_url": null, + "push_topic": "b28b41e9-8801-4fc5-ae29-8efb5fadeb36", + "components": [ + "VEVENT", + "VTODO" + ] + }, + "read_only": true + }, + { + "cal": { + "principal": "user", + "id": "calendar", + "displayname": "Calendar", + "order": 0, + "description": null, + "color": null, + "timezone_id": "Europe/Berlin", + "deleted_at": null, + "synctoken": 12, + "subscription_url": null, + "push_topic": "b28b41e9-8801-4fc5-ae29-8efb5fadeb36", + "components": [ + "VEVENT", + "VTODO" + ] + }, + "read_only": true + } +] diff --git a/crates/caldav/src/calendar/tests.rs b/crates/caldav/src/calendar/tests.rs new file mode 100644 index 0000000..7bf6a2a --- /dev/null +++ b/crates/caldav/src/calendar/tests.rs @@ -0,0 +1,47 @@ +use crate::{CalDavPrincipalUri, calendar::resource::CalendarResource}; +use rustical_dav::resource::Resource; +use rustical_store::auth::Principal; +use rustical_xml::XmlSerializeRoot; +use serde_json::from_str; + +// #[tokio::test] +async fn test_propfind() { + let requests: Vec<_> = include_str!("./test_files/propfind.requests") + .trim() + .split("\n\n") + .collect(); + let principals: Vec = + from_str(include_str!("./test_files/propfind.principals.json")).unwrap(); + let resources: Vec = + from_str(include_str!("./test_files/propfind.resources.json")).unwrap(); + let outputs: Vec<_> = include_str!("./test_files/propfind.outputs") + .trim() + .split("\n\n") + .collect(); + + for principal in principals { + for ((request, resource), &expected_output) in requests.iter().zip(&resources).zip(&outputs) + { + let propfind = CalendarResource::parse_propfind(request).unwrap(); + + let response = resource + .propfind( + &format!("/caldav/principal/{}/{}", principal.id, resource.cal.id), + &propfind.prop, + propfind.include.as_ref(), + &CalDavPrincipalUri("/caldav"), + &principal, + ) + .unwrap(); + let expected_output = expected_output.trim(); + let output = response + .serialize_to_string() + .unwrap() + .trim() + .replace("\r\n", "\n"); + println!("{output}"); + println!("{}, {} \n\n\n", output.len(), expected_output.len()); + assert_eq!(output, expected_output); + } + } +} diff --git a/crates/caldav/src/principal/tests.rs b/crates/caldav/src/principal/tests.rs index ccdd41a..e18e6e2 100644 --- a/crates/caldav/src/principal/tests.rs +++ b/crates/caldav/src/principal/tests.rs @@ -1,14 +1,19 @@ use std::sync::Arc; -use crate::principal::PrincipalResourceService; +use crate::{ + CalDavPrincipalUri, + principal::{PrincipalResource, PrincipalResourceService}, +}; use rstest::rstest; -use rustical_dav::resource::ResourceService; +use rustical_dav::resource::{Resource, ResourceService}; +use rustical_store::auth::{Principal, PrincipalType::Individual}; use rustical_store_sqlite::{ SqliteStore, calendar_store::SqliteCalendarStore, principal_store::SqlitePrincipalStore, tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store}, }; +use rustical_xml::XmlSerializeRoot; #[rstest] #[tokio::test] @@ -44,4 +49,35 @@ async fn test_principal_resource( } #[tokio::test] -async fn test_propfind() {} +async fn test_propfind() { + let propfind = PrincipalResource::parse_propfind( + r#""#, + ) + .unwrap(); + + let principal = Principal { + id: "user".to_string(), + displayname: None, + principal_type: Individual, + password: None, + memberships: vec!["group".to_string()], + }; + + let resource = PrincipalResource { + principal: principal.clone(), + members: vec![], + simplified_home_set: false, + }; + + let response = resource + .propfind( + &format!("/caldav/principal/{}", principal.id), + &propfind.prop, + propfind.include.as_ref(), + &CalDavPrincipalUri("/caldav"), + &principal, + ) + .unwrap(); + + let output = response.serialize_to_string().unwrap(); +} diff --git a/crates/ical/src/icalendar/object.rs b/crates/ical/src/icalendar/object.rs index f6d4af7..8a4295e 100644 --- a/crates/ical/src/icalendar/object.rs +++ b/crates/ical/src/icalendar/object.rs @@ -6,11 +6,12 @@ use chrono::Utc; use derive_more::Display; use ical::generator::{Emitter, IcalCalendar}; use ical::property::Property; +use serde::Deserialize; use serde::Serialize; use sha2::{Digest, Sha256}; use std::{collections::HashMap, io::BufReader}; -#[derive(Debug, Clone, Serialize, PartialEq, Eq, Display)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display)] // specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6 pub enum CalendarObjectType { #[serde(rename = "VEVENT")] diff --git a/crates/store/src/calendar.rs b/crates/store/src/calendar.rs index 7b67580..94697ab 100644 --- a/crates/store/src/calendar.rs +++ b/crates/store/src/calendar.rs @@ -3,9 +3,9 @@ use std::str::FromStr; use crate::synctoken::format_synctoken; use chrono::NaiveDateTime; use rustical_ical::CalendarObjectType; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Clone, Serialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Calendar { pub principal: String, pub id: String,