diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index d2938723..84e36ee4 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -345,6 +345,7 @@ impl Server { pub async fn recalculate_quota(&self, account_id: u32) -> trc::Result<()> { let mut quota = 0i64; + let todo = "include sieve scripts and calendars, contacts, files in quota"; self.store() .iterate( diff --git a/crates/groupware/src/calendar/index.rs b/crates/groupware/src/calendar/index.rs index 7c32232a..b4ffd9b6 100644 --- a/crates/groupware/src/calendar/index.rs +++ b/crates/groupware/src/calendar/index.rs @@ -282,13 +282,13 @@ impl ArchivedTimezone { impl DefaultAlert { pub fn size(&self) -> usize { - std::mem::size_of::() + self.id.len() + std::mem::size_of::() + self.id.len() } } impl ArchivedDefaultAlert { pub fn size(&self) -> usize { - std::mem::size_of::() + self.id.len() + std::mem::size_of::() + self.id.len() } } diff --git a/crates/jmap-proto/src/object/calendar.rs b/crates/jmap-proto/src/object/calendar.rs index 22fb55c8..4570b824 100644 --- a/crates/jmap-proto/src/object/calendar.rs +++ b/crates/jmap-proto/src/object/calendar.rs @@ -17,7 +17,7 @@ use calcard::{ jscalendar::{JSCalendarAlertAction, JSCalendarRelativeTo, JSCalendarType}, }; use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; -use std::{borrow::Cow, str::FromStr}; +use std::{borrow::Cow, fmt::Display, str::FromStr}; use types::{acl::Acl, id::Id}; #[derive(Debug, Clone, Default)] @@ -467,3 +467,9 @@ impl JmapObjectId for CalendarProperty { false } } + +impl Display for CalendarProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_cow()) + } +} diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index b3643dd1..5ab4d5da 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -181,6 +181,38 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Get, MethodObject::Calendar) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Calendar(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::Get, MethodObject::CalendarEvent) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::CalendarEvent(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::Get, MethodObject::CalendarEventNotification) => { + match seq.next_element() { + Ok(Some(value)) => { + RequestMethod::Get(GetRequestMethod::CalendarEventNotification(value)) + } + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + } + } + (MethodFunction::Get, MethodObject::ParticipantIdentity) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ParticipantIdentity(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::Get, MethodObject::AddressBook) => match seq.next_element() { Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::AddressBook(value)), Err(err) => RequestMethod::invalid(err), @@ -265,6 +297,38 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Set, MethodObject::Calendar) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Calendar(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::Set, MethodObject::CalendarEvent) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::CalendarEvent(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::Set, MethodObject::CalendarEventNotification) => { + match seq.next_element() { + Ok(Some(value)) => { + RequestMethod::Set(SetRequestMethod::CalendarEventNotification(value)) + } + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + } + } + (MethodFunction::Set, MethodObject::ParticipantIdentity) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ParticipantIdentity(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::Set, MethodObject::AddressBook) => match seq.next_element() { Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::AddressBook(value)), Err(err) => RequestMethod::invalid(err), @@ -335,6 +399,24 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Query, MethodObject::CalendarEvent) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::CalendarEvent(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::Query, MethodObject::CalendarEventNotification) => { + match seq.next_element() { + Ok(Some(value)) => { + RequestMethod::Query(QueryRequestMethod::CalendarEventNotification(value)) + } + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + } + } (MethodFunction::Query, MethodObject::ContactCard) => match seq.next_element() { Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::ContactCard(value)), Err(err) => RequestMethod::invalid(err), @@ -414,6 +496,27 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::QueryChanges, MethodObject::CalendarEvent) => match seq.next_element() + { + Ok(Some(value)) => { + RequestMethod::QueryChanges(QueryChangesRequestMethod::CalendarEvent(value)) + } + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::QueryChanges, MethodObject::CalendarEventNotification) => { + match seq.next_element() { + Ok(Some(value)) => RequestMethod::QueryChanges( + QueryChangesRequestMethod::CalendarEventNotification(value), + ), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + } + } (MethodFunction::QueryChanges, MethodObject::ContactCard) => match seq.next_element() { Ok(Some(value)) => { RequestMethod::QueryChanges(QueryChangesRequestMethod::ContactCard(value)) @@ -464,6 +567,13 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Copy, MethodObject::CalendarEvent) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::CalendarEvent(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::Copy, MethodObject::ContactCard) => match seq.next_element() { Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::ContactCard(value)), Err(err) => RequestMethod::invalid(err), @@ -499,6 +609,13 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Parse, MethodObject::CalendarEvent) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::CalendarEvent(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::Parse, MethodObject::ContactCard) => match seq.next_element() { Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::ContactCard(value)), Err(err) => RequestMethod::invalid(err), @@ -506,6 +623,17 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::GetAvailability, MethodObject::Principal) => { + match seq.next_element() { + Ok(Some(value)) => { + RequestMethod::Get(GetRequestMethod::PrincipalAvailability(value)) + } + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + } + } (MethodFunction::Validate, MethodObject::SieveScript) => match seq.next_element() { Ok(Some(value)) => RequestMethod::ValidateScript(value), Err(err) => RequestMethod::invalid(err), diff --git a/crates/jmap/src/addressbook/set.rs b/crates/jmap/src/addressbook/set.rs index fe63d823..17b1d4fd 100644 --- a/crates/jmap/src/addressbook/set.rs +++ b/crates/jmap/src/addressbook/set.rs @@ -324,14 +324,19 @@ impl AddressBookSet for Server { set_default = Some(id.document_id()); } if let Some(default_address_book_id) = set_default { - batch - .with_account_id(account_id) - .with_collection(Collection::Principal) - .update_document(0) - .set( - PrincipalField::DefaultAddressBookId, - default_address_book_id.serialize(), - ); + if response.not_created.is_empty() + && response.not_updated.is_empty() + && response.not_destroyed.is_empty() + { + batch + .with_account_id(account_id) + .with_collection(Collection::Principal) + .update_document(0) + .set( + PrincipalField::DefaultAddressBookId, + default_address_book_id.serialize(), + ); + } } else if reset_default_address_book { batch .with_account_id(account_id) @@ -341,13 +346,13 @@ impl AddressBookSet for Server { } // Write changes - if !batch.is_empty() { - let change_id = self + if !batch.is_empty() + && let Ok(change_id) = self .commit_batch(batch) .await - .and_then(|ids| ids.last_change_id(account_id)) - .caused_by(trc::location!())?; - + .caused_by(trc::location!())? + .last_change_id(account_id) + { response.new_state = State::Exact(change_id).into(); } diff --git a/crates/jmap/src/calendar/get.rs b/crates/jmap/src/calendar/get.rs index 4e24fc83..c5d66803 100644 --- a/crates/jmap/src/calendar/get.rs +++ b/crates/jmap/src/calendar/get.rs @@ -57,7 +57,8 @@ impl CalendarGet for Server { let cache = self .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await?; - let calendar_ids = if access_token.is_member(account_id) { + let is_owner = access_token.is_member(account_id); + let calendar_ids = if is_owner { cache.document_ids(true).collect::() } else { cache.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true) @@ -182,7 +183,11 @@ impl CalendarGet for Server { IncludeInAvailability::from_flags( calendar.preferences(access_token).flags.to_native(), ) - .unwrap_or_default(), + .unwrap_or(if is_owner { + IncludeInAvailability::All + } else { + IncludeInAvailability::None + }), )), ); } diff --git a/crates/jmap/src/calendar/set.rs b/crates/jmap/src/calendar/set.rs index 9858e606..70759134 100644 --- a/crates/jmap/src/calendar/set.rs +++ b/crates/jmap/src/calendar/set.rs @@ -24,7 +24,7 @@ use jmap_proto::{ request::{IntoValid, reference::MaybeIdReference}, types::state::State, }; -use jmap_tools::{JsonPointerItem, Key, Value}; +use jmap_tools::{JsonPointerItem, Key, Map, Value}; use rand::{Rng, distr::Alphanumeric}; use store::{ SerializeInfallible, ValueKey, @@ -316,14 +316,19 @@ impl CalendarSet for Server { set_default = Some(id.document_id()); } if let Some(default_calendar_id) = set_default { - batch - .with_account_id(account_id) - .with_collection(Collection::Principal) - .update_document(0) - .set( - PrincipalField::DefaultCalendarId, - default_calendar_id.serialize(), - ); + if response.not_created.is_empty() + && response.not_updated.is_empty() + && response.not_destroyed.is_empty() + { + batch + .with_account_id(account_id) + .with_collection(Collection::Principal) + .update_document(0) + .set( + PrincipalField::DefaultCalendarId, + default_calendar_id.serialize(), + ); + } } else if reset_default_calendar { batch .with_account_id(account_id) @@ -333,13 +338,13 @@ impl CalendarSet for Server { } // Write changes - if !batch.is_empty() { - let change_id = self + if !batch.is_empty() + && let Ok(change_id) = self .commit_batch(batch) .await - .and_then(|ids| ids.last_change_id(account_id)) - .caused_by(trc::location!())?; - + .caused_by(trc::location!())? + .last_change_id(account_id) + { response.new_state = State::Exact(change_id).into(); } @@ -432,7 +437,13 @@ fn update_calendar( alerts.retain(|alert| (alert.flags & ALERT_WITH_TIME != 0) != with_time); for (key, value) in value.into_vec() { - alerts.push(value_to_default_alert(key.to_string().into_owned(), value)?); + if let Value::Object(value) = value { + alerts.push(value_to_default_alert( + key.to_string().into_owned(), + value, + with_time, + )?); + } } } (CalendarProperty::ShareWith, value) => { @@ -441,7 +452,6 @@ fn update_calendar( } (CalendarProperty::Pointer(pointer), value) => { let mut ptr_iter = pointer.iter(); - ptr_iter.next(); match ptr_iter.next() { Some(JsonPointerItem::Key(Key::Property(CalendarProperty::ShareWith))) => { @@ -455,9 +465,16 @@ fn update_calendar( Some(JsonPointerItem::Key(Key::Property( property @ (CalendarProperty::DefaultAlertsWithTime | CalendarProperty::DefaultAlertsWithoutTime), - ))) => match (ptr_iter.next(), value) { - (Some(JsonPointerItem::Key(key)), value) if ptr_iter.next().is_none() => { - let id = key.to_string().into_owned(); + ))) => match (ptr_iter.next(), ptr_iter.next()) { + ( + Some(key @ (JsonPointerItem::Key(_) | JsonPointerItem::Number(_))), + None, + ) => { + let id = match key { + JsonPointerItem::Key(key) => key.to_string().into_owned(), + JsonPointerItem::Number(n) => n.to_string(), + _ => unreachable!(), + }; let with_time = matches!(property, CalendarProperty::DefaultAlertsWithTime); let alerts = &mut calendar.preferences_mut(access_token).default_alerts; @@ -465,7 +482,9 @@ fn update_calendar( (alert.flags & ALERT_WITH_TIME != 0) != with_time || alert.id != id }); - alerts.push(value_to_default_alert(id, value)?); + if let Value::Object(value) = value { + alerts.push(value_to_default_alert(id, value, with_time)?); + } } _ => { return Err(SetError::invalid_properties() @@ -500,7 +519,8 @@ fn update_calendar( fn value_to_default_alert( id: String, - value: Value<'_, CalendarProperty, CalendarValue>, + value: Map<'_, CalendarProperty, CalendarValue>, + with_time: bool, ) -> Result> { let mut alert = DefaultAlert { id, @@ -508,7 +528,7 @@ fn value_to_default_alert( }; let mut has_offset = false; - for (key, value) in value.into_expanded_object() { + for (key, value) in value.into_vec() { let Key::Property(key) = key else { continue; }; @@ -563,6 +583,10 @@ fn value_to_default_alert( } if has_offset { + if with_time { + alert.flags |= ALERT_WITH_TIME; + } + Ok(alert) } else { Err(SetError::invalid_properties() diff --git a/crates/jmap/src/calendar_event/get.rs b/crates/jmap/src/calendar_event/get.rs index 48a665a4..dd85f955 100644 --- a/crates/jmap/src/calendar_event/get.rs +++ b/crates/jmap/src/calendar_event/get.rs @@ -26,12 +26,15 @@ use groupware::{ }; use jmap_proto::{ method::get::{GetRequest, GetResponse}, - object::calendar_event, - request::reference::MaybeResultReference, + object::{JmapObjectId, calendar_event}, + request::{IntoValid, reference::MaybeResultReference}, }; -use jmap_tools::{Map, Value}; +use jmap_tools::{Key, Map, Value}; use std::sync::Arc; -use store::{ahash::AHashSet, roaring::RoaringBitmap}; +use store::{ + ahash::{AHashMap, AHashSet}, + roaring::RoaringBitmap, +}; use trc::AddContext; use types::{ acl::Acl, @@ -54,17 +57,11 @@ impl CalendarEventGet for Server { mut request: GetRequest, access_token: &AccessToken, ) -> trc::Result> { - let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let return_all_properties = request .properties .as_ref() .is_none_or(|v| matches!(v, MaybeResultReference::Value(v) if v.is_empty())); - let properties = request.unwrap_properties(&[ - JSCalendarProperty::Id, - JSCalendarProperty::CalendarIds, - JSCalendarProperty::IsDraft, - JSCalendarProperty::IsOrigin, - ]); + let properties = request.unwrap_properties(&[]); let account_id = request.account_id.document_id(); let cache = self .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) @@ -74,14 +71,29 @@ impl CalendarEventGet for Server { } else { cache.shared_items(access_token, [Acl::ReadItems], true) }; - let mut ids = if let Some(ids) = ids { - ids + let (mut ids, has_synthetic_ids) = if let Some(rr) = request.ids.take() { + let rr = rr.unwrap(); + if rr.len() > self.core.jmap.get_max_objects { + return Err(trc::JmapEvent::RequestTooLarge.into_err()); + } + let mut ids = Vec::with_capacity(rr.len()); + let mut has_synthetic_ids = false; + + for id in rr.into_valid() { + has_synthetic_ids |= id.is_synthetic(); + ids.push(id); + } + + (ids, has_synthetic_ids) } else { - calendar_event_ids - .iter() - .take(self.core.jmap.get_max_objects) - .map(Into::into) - .collect::>() + ( + calendar_event_ids + .iter() + .take(self.core.jmap.get_max_objects) + .map(Into::into) + .collect::>(), + false, + ) }; let mut response = GetResponse { account_id: request.account_id.into(), @@ -90,7 +102,7 @@ impl CalendarEventGet for Server { not_found: vec![], }; let mut return_converted_props = !return_all_properties; - let mut return_is_orgin = OriginAddresses::None; + let mut return_is_origin = false; let mut return_utc_dates = false; let (jmap_properties, jscal_properties) = if !return_all_properties { @@ -114,16 +126,7 @@ impl CalendarEventGet for Server { jmap_properties.push(property); } JSCalendarProperty::IsOrigin => { - if return_is_orgin.is_none() { - if access_token.primary_id() == account_id { - return_is_orgin = OriginAddresses::Ref(access_token); - } else { - return_is_orgin = OriginAddresses::Owned( - self.get_access_token(account_id).await?, - ); - } - jmap_properties.push(JSCalendarProperty::IsOrigin); - } + return_is_origin = true; } _ => { if matches!(property, JSCalendarProperty::ICalComponent) { @@ -136,11 +139,33 @@ impl CalendarEventGet for Server { } (jmap_properties, jscal_properties) } else { - (properties, vec![]) + return_is_origin = true; + ( + vec![ + JSCalendarProperty::Id, + JSCalendarProperty::CalendarIds, + JSCalendarProperty::IsDraft, + JSCalendarProperty::IsOrigin, + ], + vec![], + ) + }; + let return_is_origin = if return_is_origin { + if access_token.primary_id() == account_id { + OriginAddresses::Ref(access_token) + } else { + OriginAddresses::Owned(self.get_access_token(account_id).await?) + } + } else { + OriginAddresses::None }; // Sort by baseId - ids.sort_unstable_by_key(|id| id.document_id()); + let mut original_order: Option> = None; + if has_synthetic_ids { + original_order = Some(ids.iter().enumerate().map(|(i, id)| (*id, i)).collect()); + ids.sort_unstable_by_key(|id| id.document_id()); + } let mut ids = ids.into_iter().peekable(); // Process arguments @@ -403,7 +428,7 @@ impl CalendarEventGet for Server { } for (id, ical, expansion) in results { - let is_origin = return_is_orgin.addresses().is_some_and(|addresses| { + let is_origin = return_is_origin.addresses().is_some_and(|addresses| { ical.components .iter() .find(|c| c.component_type.is_scheduling_object()) @@ -438,7 +463,7 @@ impl CalendarEventGet for Server { } JSCalendarProperty::BaseEventId => { result.insert_unchecked( - JSCalendarProperty::Id, + JSCalendarProperty::BaseEventId, Value::Element(JSCalendarValue::Id(id.document_id().into())), ); } @@ -523,6 +548,20 @@ impl CalendarEventGet for Server { } } + // Restore original order + if let Some(original_order) = original_order { + response.list.sort_by_key(|obj| { + obj.as_object() + .unwrap() + .get(&Key::Property(JSCalendarProperty::::Id)) + .and_then(|v| v.as_element()) + .and_then(|v: &JSCalendarValue| v.as_id()) + .and_then(|id| original_order.get(&id)) + .cloned() + .unwrap_or(usize::MAX) + }); + } + Ok(response) } } @@ -541,8 +580,4 @@ impl<'x> OriginAddresses<'x> { _ => None, } } - - fn is_none(&self) -> bool { - matches!(self, OriginAddresses::None) - } } diff --git a/crates/jmap/src/calendar_event/mod.rs b/crates/jmap/src/calendar_event/mod.rs index 95037923..033ddf48 100644 --- a/crates/jmap/src/calendar_event/mod.rs +++ b/crates/jmap/src/calendar_event/mod.rs @@ -5,7 +5,7 @@ */ use calcard::jscalendar::JSCalendarProperty; -use common::{DavName, DavResources, Server}; +use common::Server; use jmap_proto::error::set::SetError; use store::query::Filter; use trc::AddContext; @@ -56,13 +56,11 @@ impl CalendarSyntheticId for Id { pub(super) async fn assert_is_unique_uid( server: &Server, - resources: &DavResources, account_id: u32, - calendar_ids: &[DavName], uid: Option<&str>, ) -> trc::Result>>> { - if let Some(uid) = uid { - let hits = server + if let Some(uid) = uid + && !server .store() .filter( account_id, @@ -70,28 +68,16 @@ pub(super) async fn assert_is_unique_uid( vec![Filter::eq(CalendarField::Uid, uid.as_bytes().to_vec())], ) .await - .caused_by(trc::location!())?; - if !hits.results.is_empty() { - for document_id in resources - .paths - .iter() - .filter(move |item| { - item.parent_id - .is_some_and(|id| calendar_ids.iter().any(|ab| ab.parent_id == id)) - }) - .map(|path| resources.resources[path.resource_idx].document_id) - { - if hits.results.contains(document_id) { - return Ok(Err(SetError::invalid_properties() - .with_property(JSCalendarProperty::Uid) - .with_description(format!( - "Contact with UID {uid} already exists with id {}.", - Id::from(document_id) - )))); - } - } - } + .caused_by(trc::location!())? + .results + .is_empty() + { + Ok(Err(SetError::invalid_properties() + .with_property(JSCalendarProperty::Uid) + .with_description(format!( + "An event with UID {uid} already exists.", + )))) + } else { + Ok(Ok(())) } - - Ok(Ok(())) } diff --git a/crates/jmap/src/calendar_event/query.rs b/crates/jmap/src/calendar_event/query.rs index 3b464e8c..1e697fad 100644 --- a/crates/jmap/src/calendar_event/query.rs +++ b/crates/jmap/src/calendar_event/query.rs @@ -48,19 +48,19 @@ impl CalendarEventQuery for Server { .then(|| cache.shared_items(access_token, [Acl::ReadItems], true)); let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC); let expand_recurrences = request.arguments.expand_recurrences.unwrap_or(false); - let mut filter_start = None; - let mut filter_end = None; + let mut filter: Option = None; + let mut did_filter_by_time = false; // Extract from/to arguments for cond in &request.filter { if let Filter::Property(CalendarEventFilter::After(date)) = cond { if let Some(after) = local_timestamp(date, default_tz) { - filter_start = Some(after); + filter.get_or_insert_default().start = after; } } else if let Filter::Property(CalendarEventFilter::Before(date)) = cond && let Some(before) = local_timestamp(date, default_tz) { - filter_end = Some(before); + filter.get_or_insert_default().end = before; } } @@ -83,52 +83,20 @@ impl CalendarEventQuery for Server { )); } } - CalendarEventFilter::After(_) => { - /* - The end of the event, or any recurrence of the event, - in the time zone given as the "timeZone" argument, - must be after this date to match the condition. - */ - - if let Some(filter_start) = filter_start { - if let Some(filter_end) = filter_end { - filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( - cache.resources.iter().filter_map(|r| { - r.event_time_range().and_then(|(start, end)| { - (((filter_start < end) || (filter_start <= start)) - && (filter_end > start || filter_end >= end)) - .then_some(r.document_id) - }) - }), - ))); - } else { - filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( - cache.resources.iter().filter_map(|r| { - r.event_time_range().and_then(|(_, end)| { - (end >= filter_start).then_some(r.document_id) - }) - }), - ))); - } - } - } - CalendarEventFilter::Before(_) => { - /* - The start of the event, or any recurrence of the event, - in the time zone given as the "timeZone" argument, - must be before this date to match the condition. - */ - - if filter_start.is_none() - && let Some(filter_end) = filter_end + CalendarEventFilter::After(_) | CalendarEventFilter::Before(_) => { + if let Some(filter) = &filter + && !did_filter_by_time { filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( cache.resources.iter().filter_map(|r| { - r.event_time_range().and_then(|(start, _)| { - (start < filter_end).then_some(r.document_id) + r.event_time_range().and_then(|(start, end)| { + filter + .is_in_range(false, start, end) + .then_some(r.document_id) }) }), ))); + did_filter_by_time = true; } } unsupported => { @@ -152,20 +120,21 @@ impl CalendarEventQuery for Server { result_set.apply_mask(filter_mask); } - let (mut response, paginate) = self - .build_query_response(&result_set, cache.get_state(false), &request) - .await?; - - if let Some(mut paginate) = paginate { + let num_results = result_set.results.len() as usize; + if num_results > 0 { // Extract comparators - let comparators = request.sort.filter(|s| !s.is_empty()).unwrap_or_default(); + let comparators = request + .sort + .as_deref() + .filter(|s| !s.is_empty()) + .unwrap_or_default(); if expand_recurrences { - let (Some(start), Some(end)) = (filter_start, filter_end) else { + let Some(time_range) = filter.filter(|f| f.start != i64::MIN && f.end != i64::MAX) + else { return Err(trc::JmapEvent::InvalidArguments.into_err().details( "Both 'after' and 'before' filters are required when expanding recurrences", )); }; - let time_range = TimeRange { start, end }; let max_instances = self.core.groupware.max_ical_instances; let mut results = Vec::with_capacity(result_set.results.len() as usize); let has_uid_comparator = comparators @@ -222,15 +191,15 @@ impl CalendarEventQuery for Server { // Sort results if !results.is_empty() { results.sort_by(|a, b| { - for comparator in &comparators { + for comparator in comparators { let ordering = a .get_property(&comparator.property) .cmp(b.get_property(&comparator.property)); let ordering = if comparator.is_ascending { - ordering - } else { ordering.reverse() + } else { + ordering }; if ordering != Ordering::Equal { @@ -242,19 +211,24 @@ impl CalendarEventQuery for Server { } // Add results - for result in results { - if !paginate.add(result.expansion_id + 1, result.document_id) { - break; + let (mut response, paginate) = self + .build_query_response(results.len(), cache.get_state(false), &request) + .await?; + if let Some(mut paginate) = paginate { + for result in results { + if !paginate.add(result.expansion_id + 1, result.document_id) { + break; + } } + response.update_results(paginate.build())?; } - response.update_results(paginate.build())?; Ok(response) } else { let mut comparators_ = Vec::with_capacity(comparators.len()); for comparator in comparators { - comparators_.push(match comparator.property { + comparators_.push(match &comparator.property { CalendarEventComparator::Uid => { query::Comparator::field(CalendarField::Uid, comparator.is_ascending) } @@ -272,16 +246,31 @@ impl CalendarEventQuery for Server { unsupported => { return Err(trc::JmapEvent::UnsupportedSort .into_err() - .details(unsupported.into_string())); + .details(unsupported.clone().into_string())); } }); } // Sort results - self.sort(result_set, comparators_, paginate, response) - .await + let (response, paginate) = self + .build_query_response(num_results, cache.get_state(false), &request) + .await?; + if let Some(paginate) = paginate { + self.sort(result_set, comparators_, paginate, response) + .await + } else { + Ok(response) + } } } else { + let (response, _) = self + .build_query_response( + result_set.results.len() as usize, + cache.get_state(false), + &request, + ) + .await?; + Ok(response) } } @@ -293,6 +282,7 @@ fn local_timestamp(dt: &JSCalendarDateTime, tz: Tz) -> Option { .map(|dt| dt.timestamp()) } +#[derive(Debug)] struct SearchResult { expansion_id: u32, document_id: u32, diff --git a/crates/jmap/src/calendar_event/set.rs b/crates/jmap/src/calendar_event/set.rs index 695c149e..a5631e7e 100644 --- a/crates/jmap/src/calendar_event/set.rs +++ b/crates/jmap/src/calendar_event/set.rs @@ -555,7 +555,7 @@ impl CalendarEventSet for Server { "You are not allowed to add calendar events to calendar {}.", Id::from(name.parent_id) )))); - } else if let Some(show_with_time) = use_default_alerts + } else if let Some(show_without_time) = use_default_alerts && let Some(_calendar) = self .get_archive(account_id, Collection::Calendar, name.parent_id) .await? @@ -564,7 +564,7 @@ impl CalendarEventSet for Server { _calendar .unarchive::() .caused_by(trc::location!())? - .default_alerts(access_token, show_with_time) + .default_alerts(access_token, !show_without_time) .map(default_alert_to_ical), ); } @@ -583,9 +583,7 @@ impl CalendarEventSet for Server { } // Validate UID - if let Err(err) = - assert_is_unique_uid(self, cache, account_id, &event.names, ical.uids().next()).await? - { + if let Err(err) = assert_is_unique_uid(self, account_id, ical.uids().next()).await? { return Ok(Err(err)); } diff --git a/crates/jmap/src/calendar_event_notification/query.rs b/crates/jmap/src/calendar_event_notification/query.rs index 03c315b9..bc27349d 100644 --- a/crates/jmap/src/calendar_event_notification/query.rs +++ b/crates/jmap/src/calendar_event_notification/query.rs @@ -93,7 +93,11 @@ impl CalendarEventNotificationQuery for Server { .await?; let (response, paginate) = self - .build_query_response(&result_set, cache.get_state(false), &request) + .build_query_response( + result_set.results.len() as usize, + cache.get_state(false), + &request, + ) .await?; if let Some(paginate) = paginate { diff --git a/crates/jmap/src/contact/query.rs b/crates/jmap/src/contact/query.rs index 5f5504e0..01d41beb 100644 --- a/crates/jmap/src/contact/query.rs +++ b/crates/jmap/src/contact/query.rs @@ -105,7 +105,7 @@ impl ContactCardQuery for Server { } let (response, paginate) = self - .build_query_response(&result_set, cache.get_state(false), &request) + .build_query_response(result_set.results.len() as usize, cache.get_state(false), &request) .await?; if let Some(paginate) = paginate { diff --git a/crates/jmap/src/email/query.rs b/crates/jmap/src/email/query.rs index 45bd7f7d..f3937aef 100644 --- a/crates/jmap/src/email/query.rs +++ b/crates/jmap/src/email/query.rs @@ -325,7 +325,11 @@ impl EmailQuery for Server { result_set.apply_mask(cached_messages.shared_messages(access_token, Acl::ReadItems)); } let (response, paginate) = self - .build_query_response(&result_set, cached_messages.get_state(false), &request) + .build_query_response( + result_set.results.len() as usize, + cached_messages.get_state(false), + &request, + ) .await?; if let Some(paginate) = paginate { diff --git a/crates/jmap/src/file/query.rs b/crates/jmap/src/file/query.rs index 686bf035..1f8877ba 100644 --- a/crates/jmap/src/file/query.rs +++ b/crates/jmap/src/file/query.rs @@ -138,7 +138,7 @@ impl FileNodeQuery for Server { } let (response, paginate) = self - .build_query_response(&result_set, cache.get_state(false), &request) + .build_query_response(result_set.results.len() as usize, cache.get_state(false), &request) .await?; if let Some(paginate) = paginate { diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index d3b3146f..fc129931 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -97,11 +97,10 @@ impl JmapMethods for Server { async fn build_query_response( &'_ self, - result_set: &ResultSet, + total: usize, query_state: State, request: &QueryRequest, ) -> trc::Result<(QueryResponse, Option>)> { - let total = result_set.results.len() as usize; let (limit_total, limit) = if let Some(limit) = request.limit { if limit > 0 { let limit = std::cmp::min(limit, self.core.jmap.query_max_results); @@ -193,7 +192,7 @@ pub trait JmapMethods: Sync + Send { fn build_query_response( &'_ self, - result_set: &ResultSet, + total: usize, query_state: State, request: &QueryRequest, ) -> impl Future>)>> + Send; diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index 8bc52c03..c9f0a0fe 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -160,7 +160,11 @@ impl MailboxQuery for Server { result_set.apply_mask(mailboxes.shared_mailboxes(access_token, Acl::Read)); } let (mut response, mut paginate) = self - .build_query_response(&result_set, mailboxes.get_state(true), &request) + .build_query_response( + result_set.results.len() as usize, + mailboxes.get_state(true), + &request, + ) .await?; // Filter as tree diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index f1a39141..202e28ba 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -182,7 +182,7 @@ impl PrincipalQuery for Server { } let (response, paginate) = self - .build_query_response(&result_set, State::Initial, &request) + .build_query_response(result_set.results.len() as usize, State::Initial, &request) .await?; if let Some(paginate) = paginate { diff --git a/crates/jmap/src/share_notification/query.rs b/crates/jmap/src/share_notification/query.rs index 7ba46e1c..e4f413b3 100644 --- a/crates/jmap/src/share_notification/query.rs +++ b/crates/jmap/src/share_notification/query.rs @@ -123,7 +123,7 @@ impl ShareNotificationQuery for Server { .caused_by(trc::location!())?; let (mut response, paginate) = self - .build_query_response(&result_set, State::Initial, &request) + .build_query_response(result_set.results.len() as usize, State::Initial, &request) .await?; if let Some(mut paginate) = paginate { diff --git a/crates/jmap/src/sieve/query.rs b/crates/jmap/src/sieve/query.rs index 736e3609..23ca2a38 100644 --- a/crates/jmap/src/sieve/query.rs +++ b/crates/jmap/src/sieve/query.rs @@ -82,7 +82,7 @@ impl SieveScriptQuery for Server { let (response, paginate) = self .build_query_response( - &result_set, + result_set.results.len() as usize, self.get_state(account_id, SyncCollection::SieveScript) .await?, &request, diff --git a/crates/jmap/src/submission/query.rs b/crates/jmap/src/submission/query.rs index c816afe8..e057cca5 100644 --- a/crates/jmap/src/submission/query.rs +++ b/crates/jmap/src/submission/query.rs @@ -108,7 +108,7 @@ impl EmailSubmissionQuery for Server { let (response, paginate) = self .build_query_response( - &result_set, + result_set.results.len() as usize, self.get_state(account_id, SyncCollection::EmailSubmission) .await?, &request, diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 20487209..690a6a76 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -37,3 +37,12 @@ impl TimeRange { } } } + +impl Default for TimeRange { + fn default() -> Self { + Self { + start: i64::MIN, + end: i64::MAX, + } + } +} diff --git a/tests/src/jmap/calendar/calendars.rs b/tests/src/jmap/calendar/calendars.rs index f6336379..fd8e1dc4 100644 --- a/tests/src/jmap/calendar/calendars.rs +++ b/tests/src/jmap/calendar/calendars.rs @@ -4,11 +4,369 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, JmapUtils}; -use jmap_proto::request::method::MethodObject; +use crate::jmap::{ChangeType, JMAPTest, JmapUtils}; +use jmap_proto::{object::calendar::CalendarProperty, request::method::MethodObject}; use serde_json::json; pub async fn test(params: &mut JMAPTest) { - println!("Running tests..."); + println!("Running Calendar tests..."); let account = params.account("jdoe@example.com"); + + // Make sure the default calendar exists + let response = account + .jmap_get( + MethodObject::Calendar, + [ + CalendarProperty::Id, + CalendarProperty::Name, + CalendarProperty::Description, + CalendarProperty::SortOrder, + CalendarProperty::Color, + CalendarProperty::TimeZone, + CalendarProperty::IsSubscribed, + CalendarProperty::IsDefault, + CalendarProperty::IsVisible, + CalendarProperty::IncludeInAvailability, + CalendarProperty::DefaultAlertsWithTime, + CalendarProperty::DefaultAlertsWithoutTime, + ], + Vec::<&str>::new(), + ) + .await; + let list = response.list(); + assert_eq!(list.len(), 1); + let default_calendar_id = list[0].id().to_string(); + assert_eq!( + list[0], + json!({ + "id": default_calendar_id, + "name": "Stalwart Calendar (jdoe@example.com)", + "description": null, + "sortOrder": 0, + "isSubscribed": false, + "isDefault": true, + "color": null, + "timeZone": null, + "isVisible": true, + "includeInAvailability": "all", + "defaultAlertsWithTime": {}, + "defaultAlertsWithoutTime": {} + }) + ); + let change_id = response.state(); + + // Create Calendar + let calendar_id = account + .jmap_create( + MethodObject::Calendar, + [json!({ + "name": "Test calendar", + "description": "My personal calendar", + "sortOrder": 1, + "isSubscribed": true, + "color": "#ff0000", + "timeZone": "Indian/Christmas", + "isVisible": false, + "includeInAvailability": "attending", + "defaultAlertsWithTime": { + "0": { + "action": "display", + "trigger": { + "relativeTo": "start", + "offset": "PT15M" + } + }, + "1": { + "action": "email", + "trigger": { + "relativeTo": "end", + "offset": "PT30M" + } + } + }, + "defaultAlertsWithoutTime": { + "0": { + "action": "display", + "trigger": { + "relativeTo": "start", + "offset": "P1D" + } + }, + "1": { + "action": "email", + "trigger": { + "relativeTo": "end", + "offset": "P2D" + } + } + } + })], + ) + .await + .created(0) + .id() + .to_string(); + + // Validate changes + assert_eq!( + account + .jmap_changes(MethodObject::Calendar, change_id) + .await + .changes() + .collect::>(), + [ChangeType::Created(&calendar_id)] + ); + + // Get Calendar + let response = account + .jmap_get( + MethodObject::Calendar, + [ + CalendarProperty::Id, + CalendarProperty::Name, + CalendarProperty::Description, + CalendarProperty::SortOrder, + CalendarProperty::Color, + CalendarProperty::TimeZone, + CalendarProperty::IsSubscribed, + CalendarProperty::IsDefault, + CalendarProperty::IsVisible, + CalendarProperty::IncludeInAvailability, + CalendarProperty::DefaultAlertsWithTime, + CalendarProperty::DefaultAlertsWithoutTime, + ], + [&calendar_id], + ) + .await; + + response.list()[0].assert_is_equal(json!({ + "name": "Test calendar", + "description": "My personal calendar", + "sortOrder": 1, + "isSubscribed": true, + "isVisible": false, + "isDefault": false, + "color": "#ff0000", + "timeZone": "Indian/Christmas", + "includeInAvailability": "attending", + "defaultAlertsWithTime": { + "0": { + "@type": "Alert", + "action": "display", + "trigger": { + "@type": "OffsetTrigger", + "relativeTo": "start", + "offset": "PT15M" + } + }, + "1": { + "@type": "Alert", + "action": "email", + "trigger": { + "@type": "OffsetTrigger", + "relativeTo": "end", + "offset": "PT30M" + } + } + }, + "defaultAlertsWithoutTime": { + "0": { + "@type": "Alert", + "action": "display", + "trigger": { + "@type": "OffsetTrigger", + "relativeTo": "start", + "offset": "P1D" + } + }, + "1": { + "@type": "Alert", + "action": "email", + "trigger": { + "@type": "OffsetTrigger", + "relativeTo": "end", + "offset": "P2D" + } + } + }, + "id": calendar_id, + })); + + // Update Calendar and set it as default + account + .jmap_update( + MethodObject::Calendar, + [( + calendar_id.as_str(), + json!({ + "name": "Updated calendar", + "description": "My updated personal calendar", + "sortOrder": 2, + "isSubscribed": false, + "isVisible": true, + "timeZone": null, + "color": null, + "includeInAvailability": "none", + "defaultAlertsWithTime": { + "0": { + "action": "email", + "trigger": { + "relativeTo": "start", + "offset": "PT10M" + } + } + }, + "defaultAlertsWithoutTime/0": { + "action": "email", + "trigger": { + "relativeTo": "start", + "offset": "P3D" + } + }, + "defaultAlertsWithoutTime/1": null, + "defaultAlertsWithoutTime/2": { + "action": "display", + "trigger": { + "relativeTo": "end", + "offset": "P1W" + } + } + }), + )], + [("onSuccessSetIsDefault", calendar_id.as_str())], + ) + .await + .updated(&calendar_id); + + // Validate changes + let response = account + .jmap_get( + MethodObject::Calendar, + [ + CalendarProperty::Id, + CalendarProperty::Name, + CalendarProperty::Description, + CalendarProperty::SortOrder, + CalendarProperty::Color, + CalendarProperty::TimeZone, + CalendarProperty::IsSubscribed, + CalendarProperty::IsDefault, + CalendarProperty::IsVisible, + CalendarProperty::IncludeInAvailability, + CalendarProperty::DefaultAlertsWithTime, + CalendarProperty::DefaultAlertsWithoutTime, + ], + [&calendar_id, &default_calendar_id], + ) + .await; + response.list()[0].assert_is_equal(json!({ + "id": calendar_id, + "name": "Updated calendar", + "description": "My updated personal calendar", + "sortOrder": 2, + "isSubscribed": false, + "isDefault": true, + "color": null, + "timeZone": null, + "isVisible": true, + "includeInAvailability": "none", + "defaultAlertsWithTime": { + "0": { + "@type": "Alert", + "action": "email", + "trigger": { + "@type": "OffsetTrigger", + "relativeTo": "start", + "offset": "PT10M" + } + } + }, + "defaultAlertsWithoutTime": { + "0": { + "@type": "Alert", + "action": "email", + "trigger": { + "@type": "OffsetTrigger", + "relativeTo": "start", + "offset": "P3D" + } + }, + "2": { + "@type": "Alert", + "action": "display", + "trigger": { + "@type": "OffsetTrigger", + "relativeTo": "end", + "offset": "P1W" + } + } + } + })); + response.list()[1].assert_is_equal(json!({ + "id": default_calendar_id, + "name": "Stalwart Calendar (jdoe@example.com)", + "description": (), + "sortOrder": 0, + "isSubscribed": false, + "isDefault": false, + "color": null, + "timeZone": null, + "isVisible": true, + "includeInAvailability": "all", + "defaultAlertsWithTime": {}, + "defaultAlertsWithoutTime": {} + })); + + // Create an event + let _ = account + .jmap_create( + MethodObject::CalendarEvent, + [json!({ + "calendarIds": { + &calendar_id: true + }, + "@type": "Event", + "uid": "a8df6573-0474-496d-8496-033ad45d7fea", + "updated": "2020-01-02T18:23:04Z", + "title": "Some event", + "start": "2020-01-15T13:00:00", + "timeZone": "America/New_York", + "duration": "PT1H" + })], + ) + .await + .created(0) + .id(); + + // Try destroying the calendar (should fail) + assert_eq!( + account + .jmap_destroy( + MethodObject::Calendar, + [&calendar_id], + Vec::<(&str, &str)>::new(), + ) + .await + .not_destroyed(&calendar_id) + .typ(), + "calendarHasEvent" + ); + + // Destroy using force + assert_eq!( + account + .jmap_destroy( + MethodObject::Calendar, + [&calendar_id], + [("onDestroyRemoveEvents", true)], + ) + .await + .destroyed() + .collect::>(), + vec![&calendar_id] + ); + + // Destroy all mailboxes + account.destroy_all_calendars().await; + params.assert_is_empty().await; } diff --git a/tests/src/jmap/calendar/event.rs b/tests/src/jmap/calendar/event.rs index f63876cd..807c4bad 100644 --- a/tests/src/jmap/calendar/event.rs +++ b/tests/src/jmap/calendar/event.rs @@ -4,13 +4,780 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ - use crate::jmap::{JMAPTest, JmapUtils}; +use crate::{ + jmap::{ChangeType, IntoJmapSet, JMAPTest, JmapUtils}, + webdav::DummyWebDavClient, +}; +use ahash::AHashSet; +use calcard::jscalendar::JSCalendarProperty; +use groupware::cache::GroupwareCache; +use hyper::StatusCode; use jmap_proto::request::method::MethodObject; -use serde_json::json; +use serde_json::{Value, json}; +use types::{collection::SyncCollection, id::Id}; pub async fn test(params: &mut JMAPTest) { println!("Running tests..."); let account = params.account("jdoe@example.com"); + // Create test calendars + let response = account + .jmap_create( + MethodObject::Calendar, + [ + json!({ + "name": "Holy Calendar, Batman!", + "timeZone": "Europe/Vatican", + }), + json!({ + "name": "Calendar with Alerts", + "defaultAlertsWithTime": { + "abc": { + "action": "display", + "trigger": { + "relativeTo": "start", + "offset": "PT15M" + } + } + }, + }), + ], + ) + .await; + let calendar1_id = response.created(0).id().to_string(); + let calendar2_id = response.created(1).id().to_string(); + // Obtain state + let change_id = account + .jmap_get( + MethodObject::CalendarEvent, + Vec::<&str>::new(), + Vec::<&str>::new(), + ) + .await + .state() + .to_string(); + + // Create test events + let event_1 = test_jscalendar_1().with_property( + JSCalendarProperty::::CalendarIds, + [calendar1_id.as_str()].into_jmap_set(), + ); + let event_2 = test_jscalendar_2().with_property( + JSCalendarProperty::::CalendarIds, + [calendar2_id.as_str()].into_jmap_set(), + ); + let event_3 = test_jscalendar_3().with_property( + JSCalendarProperty::::CalendarIds, + [calendar1_id.as_str(), calendar2_id.as_str()].into_jmap_set(), + ); + let response = account + .jmap_create( + MethodObject::CalendarEvent, + [ + event_1 + .clone() + .with_property(JSCalendarProperty::::IsDraft, true) + .with_property(JSCalendarProperty::::MayInviteSelf, true) + .with_property(JSCalendarProperty::::MayInviteOthers, true) + .with_property(JSCalendarProperty::::HideAttendees, true), + event_2 + .clone() + .with_property(JSCalendarProperty::::UseDefaultAlerts, true), + event_3.clone(), + ], + ) + .await; + let event_1_id = response.created(0).id().to_string(); + let event_2_id = response.created(1).id().to_string(); + let event_3_id = response.created(2).id().to_string(); + + // Validate changes + assert_eq!( + account + .jmap_changes(MethodObject::CalendarEvent, &change_id) + .await + .changes() + .collect::>(), + [ + ChangeType::Created(&event_1_id), + ChangeType::Created(&event_2_id), + ChangeType::Created(&event_3_id) + ] + .into_iter() + .collect::>(), + ); + + // Verify event contents + let response = account + .jmap_get( + MethodObject::CalendarEvent, + Vec::<&str>::new(), + [&event_1_id, &event_2_id, &event_3_id], + ) + .await; + + response.list()[0].assert_is_equal( + event_1 + .with_property(JSCalendarProperty::::Id, event_1_id.as_str()) + .with_property(JSCalendarProperty::::IsDraft, true) + .with_property(JSCalendarProperty::::IsOrigin, true), + ); + response.list()[1].assert_is_equal( + event_2 + .with_property(JSCalendarProperty::::Id, event_2_id.as_str()) + .with_property(JSCalendarProperty::::IsDraft, false) + .with_property(JSCalendarProperty::::IsOrigin, true) + .with_property( + JSCalendarProperty::::Alerts, + json!({ + "k1": { + "action": "display", + "trigger": { + "@type": "OffsetTrigger", + "offset": "PT15M" + }, + "@type": "Alert" + } + }), + ), + ); + response.list()[2].assert_is_equal( + event_3 + .with_property(JSCalendarProperty::::Id, event_3_id.as_str()) + .with_property(JSCalendarProperty::::IsDraft, false) + .with_property(JSCalendarProperty::::IsOrigin, false), + ); + + // Verify JMAP for Calendars properties + let response = account + .jmap_get( + MethodObject::CalendarEvent, + [ + JSCalendarProperty::::Id, + JSCalendarProperty::MayInviteSelf, + JSCalendarProperty::MayInviteOthers, + JSCalendarProperty::HideAttendees, + JSCalendarProperty::UtcStart, + JSCalendarProperty::UtcEnd, + ], + [&event_1_id, &event_2_id, &event_3_id], + ) + .await; + response.list()[0].assert_is_equal(json!({ + "id": &event_1_id, + "mayInviteSelf": true, + "mayInviteOthers": true, + "hideAttendees": true, + "utcStart": "2006-01-02T15:00:00Z", + "utcEnd": "2006-01-02T16:00:00Z" + })); + response.list()[1].assert_is_equal(json!({ + "id": &event_2_id, + "mayInviteSelf": false, + "mayInviteOthers": false, + "hideAttendees": false, + "utcStart": "2006-01-02T17:00:00Z", + "utcEnd": "2006-01-02T18:00:00Z" + })); + response.list()[2].assert_is_equal(json!({ + "id": &event_3_id, + "mayInviteSelf": false, + "mayInviteOthers": false, + "hideAttendees": false, + "utcStart": "2006-01-04T15:00:00Z", + "utcEnd": "2006-01-04T16:00:00Z" + })); + + // Test /get parameters + let response = account + .jmap_method_calls(json!([[ + "CalendarEvent/get", + { + "properties": ["id", "title", "recurrenceOverrides", "participants"], + "ids": [&event_2_id, &event_3_id], + "recurrenceOverridesBefore": "2006-01-07T00:00:00Z", + "recurrenceOverridesAfter": "2006-01-06T00:00:00Z", + "reduceParticipants": true, + }, + "0" + ]])) + .await; + response.list_array().assert_is_equal(json!([ + { + "title": "Event #2", + "recurrenceOverrides": { + "2006-01-06T12:00:00": { + "updated": "2006-02-06T00:11:21Z", + "start": "2006-01-06T14:00:00", + "title": "Event #2 bis bis", + "duration": "PT1H" + } + }, + "id": "c" + }, + { + "title": "Event #3", + "participants": { + "3f5bc8c0-c722-5345-b7d9-5a899db08a30": { + "calendarAddress": "mailto:cyrus@example.com", + "@type": "Participant" + } + }, + "id": "d" + } + ])); + + // Creating an event without calendar should fail + assert_eq!( + account + .jmap_create( + MethodObject::CalendarEvent, + [json!({ + "title": "Event #5", + "start": "2006-01-22T10:00:00", + "duration": "PT1H", + "timeZone": "US/Eastern", + "calendarIds": {}, + }),], + ) + .await + .not_created(0) + .description(), + "Event has to belong to at least one calendar." + ); + + // Creating an event with a duplicate UID should fail + assert_eq!( + account + .jmap_create( + MethodObject::CalendarEvent, + [json!({ + "title": "Event #5", + "start": "2006-01-22T10:00:00", + "duration": "PT1H", + "timeZone": "US/Eastern", + "uid": "00959BC664CA650E933C892C@example.com", + "calendarIds": { + &calendar1_id: true + }, + })], + ) + .await + .not_created(0) + .description(), + "An event with UID 00959BC664CA650E933C892C@example.com already exists." + ); + + // Patching tests + let response = account + .jmap_update( + MethodObject::CalendarEvent, + [ + ( + &event_1_id, + json!({ + "isDraft": false, + "mayInviteSelf": false, + "mayInviteOthers": false, + "hideAttendees": false, + "description": null, + "title": "Event one", + "keywords": {"work": true}, + format!("calendarIds/{calendar2_id}"): true + }), + ), + ( + &event_2_id, + json!({ + "calendarIds": { + &calendar1_id: true, + &calendar2_id: true + }, + "title": "Event two", + "description": "Updated description", + "recurrenceOverrides/2006-01-04T12:00:00/title": + "Event two overridden", + "recurrenceOverrides/2006-01-06T12:00:00/title": + "Event two overridden twice", + + }), + ), + ( + &event_3_id, + json!({ + format!("calendarIds/{calendar2_id}"): false, + "title": "Event three", + "utcStart": "2006-01-04T14:00:00Z", + "utcEnd": "2006-01-04T16:00:00Z", + "participants/3f5bc8c0-c722-5345-b7d9-5a899db08a30/roles/chair": false, + "participants/3f5bc8c0-c722-5345-b7d9-5a899db08a30/roles/owner": true, + "participants/ec5e7db5-22a3-5ed5-89bf-c8894ab86805" : null, + "participants/7f2bd210-6c66-5b64-8562-0176b74462b1": { + "calendarAddress": "mailto:rupert@example.com", + "@type": "Participant", + "roles": { + "attendee": true + }, + "participationStatus": "needs-action" + } + }), + ), + ], + Vec::<(&str, &str)>::new(), + ) + .await; + + response.updated(&event_1_id); + response.updated(&event_2_id); + response.updated(&event_3_id); + + // Verify patches + let response = account + .jmap_get( + MethodObject::CalendarEvent, + [ + JSCalendarProperty::::Id, + JSCalendarProperty::CalendarIds, + JSCalendarProperty::Title, + JSCalendarProperty::Start, + JSCalendarProperty::Description, + JSCalendarProperty::Keywords, + JSCalendarProperty::RecurrenceOverrides, + JSCalendarProperty::Participants, + JSCalendarProperty::MayInviteOthers, + JSCalendarProperty::MayInviteSelf, + JSCalendarProperty::HideAttendees, + JSCalendarProperty::IsDraft, + ], + [&event_1_id, &event_2_id, &event_3_id], + ) + .await; + + response.list()[0].assert_is_equal(json!({ + "id": &event_1_id, + "calendarIds": { + &calendar1_id: true, + &calendar2_id: true + }, + "isDraft": false, + "mayInviteSelf": false, + "mayInviteOthers": false, + "hideAttendees": false, + "title": "Event one", + "start": "2006-01-02T10:00:00", + "keywords": { + "work": true + } + })); + + response.list()[1].assert_is_equal(json!({ + "id": &event_2_id, + "calendarIds": { + &calendar1_id: true, + &calendar2_id: true + }, + "title": "Event two", + "start": "2006-01-02T12:00:00", + "description": "Updated description", + "recurrenceOverrides": { + "2006-01-04T12:00:00": { + "title": "Event two overridden", + "start": "2006-01-04T14:00:00", + "duration": "PT1H", + "updated": "2006-02-06T00:11:21Z" + }, + "2006-01-06T12:00:00": { + "title": "Event two overridden twice", + "start": "2006-01-06T14:00:00", + "duration": "PT1H", + "updated": "2006-02-06T00:11:21Z" + } + }, + "title": "Event two", + "start": "2006-01-02T12:00:00", + "mayInviteOthers": false, + "mayInviteSelf": false, + "hideAttendees": false, + "isDraft": false + })); + + response.list()[2].assert_is_equal(json!({ + "id": event_3_id, + "calendarIds": { + &calendar1_id: true, + }, + "title": "Event three", + "start": "2006-01-04T09:00:00", + "participants": { + "3f5bc8c0-c722-5345-b7d9-5a899db08a30": { + "calendarAddress": "mailto:cyrus@example.com", + "@type": "Participant", + "roles": { + "attendee": true, + "owner": true + }, + "participationStatus": "accepted" + }, + "7f2bd210-6c66-5b64-8562-0176b74462b1": { + "calendarAddress": "mailto:rupert@example.com", + "@type": "Participant", + "roles": { + "attendee": true + }, + "participationStatus": "needs-action" + } + }, + "mayInviteOthers": false, + "mayInviteSelf": false, + "hideAttendees": false, + "isDraft": false + })); + + // Query tests + assert_eq!( + account + .jmap_query( + MethodObject::CalendarEvent, + [ + ("text", "Event one"), + ("inCalendar", calendar1_id.as_str()), + ("uid", "74855313FA803DA593CD579A@example.com"), + ("after", "2006-01-02T10:59:59"), + ("before", "2006-01-02T10:00:01"), + ], + ["start"], + [("timeZone", "US/Eastern")], + ) + .await + .ids() + .collect::>(), + [event_1_id.as_str()].into_iter().collect::>() + ); + + // Recurrence expansion tests + let response = account + .jmap_query( + MethodObject::CalendarEvent, + [ + ("after", "2006-01-01T00:00:00"), + ("before", "2006-01-08T00:00:00"), + ], + ["start"], + [ + ("timeZone", Value::String("US/Eastern".into())), + ("expandRecurrences", Value::Bool(true)), + ], + ) + .await; + let ids = response.ids().collect::>(); + assert_eq!(ids.len(), 7); + account + .jmap_get( + MethodObject::CalendarEvent, + [ + JSCalendarProperty::::Id, + JSCalendarProperty::BaseEventId, + JSCalendarProperty::Start, + JSCalendarProperty::Duration, + JSCalendarProperty::Title, + JSCalendarProperty::RecurrenceId, + ], + ids.clone(), + ) + .await + .list_array() + .assert_is_equal(json!([ + { + "duration": "PT1H", + "title": "Event one", + "start": "2006-01-02T15:00:00", + "id": &ids[0], + "baseEventId": &event_1_id + }, + { + "recurrenceId": "2006-01-02T17:00:00", + "title": "Event two", + "duration": "PT1H", + "start": "2006-01-02T17:00:00", + "id": &ids[1], + "baseEventId": &event_2_id + }, + { + "duration": "PT1H", + "start": "2006-01-03T17:00:00", + "title": "Event two", + "recurrenceId": "2006-01-03T17:00:00", + "id": &ids[2], + "baseEventId": &event_2_id + }, + { + "start": "2006-01-04T14:00:00", + "duration": "PT2H", + "title": "Event three", + "id": &ids[3], + "baseEventId": &event_3_id + }, + { + "recurrenceId": "2006-01-04T19:00:00", + "title": "Event two overridden", + "start": "2006-01-04T19:00:00", + "duration": "PT1H", + "id": &ids[4], + "baseEventId": &event_2_id + }, + { + "recurrenceId": "2006-01-05T17:00:00", + "duration": "PT1H", + "start": "2006-01-05T17:00:00", + "title": "Event two", + "id": &ids[5], + "baseEventId": &event_2_id + }, + { + "recurrenceId": "2006-01-06T19:00:00", + "duration": "PT1H", + "title": "Event two overridden twice", + "start": "2006-01-06T19:00:00", + "id": &ids[6], + "baseEventId": &event_2_id + } + ])); + + // Parse tests + account + .jmap_method_calls(json!([ + [ + "Blob/upload", + { + "create": { + "ical": { + "data": [ + { + "data:asText": r#"BEGIN:VCALENDAR +PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN +VERSION:2.0 +BEGIN:VEVENT +DTSTAMP:19960704T120000Z +UID:uid1@example.com +ORGANIZER:mailto:jsmith@example.com +DTSTART:19960918T143000Z +DTEND:19960920T220000Z +STATUS:CONFIRMED +CATEGORIES:CONFERENCE +SUMMARY:Networld+Interop Conference +DESCRIPTION:Networld+Interop Conference + and Exhibit\nAtlanta World Congress Center\n +Atlanta\, Georgia +END:VEVENT +END:VCALENDAR +"# + } + ] + } + } + }, + "S4" + ], + [ + "CalendarEvent/parse", + { + "blobIds": [ + "#ical" + ] + }, + "G4" + ] + ])) + .await + .pointer("/methodResponses/1/1/parsed") + .unwrap() + .as_object() + .unwrap() + .iter() + .next() + .unwrap() + .1 + .assert_is_equal(json!([ + { + "updated": "1996-07-04T12:00:00Z", + "title": "Networld+Interop Conference", + "description": "Networld+Interop Conferenceand Exhibit\nAtlanta World Congress Center\n", + "timeZone": "Etc/UTC", + "start": "1996-09-18T14:30:00", + "status": "confirmed", + "iCalComponent": { + "convertedProperties": { + "duration": { + "name": "DTEND" + } + }, + "name": "vevent" + }, + "@type": "Event", + "uid": "uid1@example.com", + "participants": { + "25d7647e-52fc-559b-88df-d66f08da079c": { + "calendarAddress": "mailto:jsmith@example.com", + "@type": "Participant" + } + }, + "keywords": { + "CONFERENCE": true + }, + "organizerCalendarAddress": "mailto:jsmith@example.com", + "duration": "P2DT7H30M" + } +])); + + // Deletion tests + assert_eq!( + account + .jmap_destroy( + MethodObject::CalendarEvent, + [event_2_id.as_str(), event_3_id.as_str()], + Vec::<(&str, &str)>::new() + ) + .await + .destroyed() + .collect::>(), + [event_2_id.as_str(), event_3_id.as_str()] + .into_iter() + .collect::>() + ); + + // CardDAV compatibility tests + let account_id = account.id().document_id(); + let dav_client = DummyWebDavClient::new( + u32::MAX, + account.name(), + account.secret(), + account.emails()[0], + ); + let resources = params + .server + .fetch_dav_resources( + ¶ms.server.get_access_token(account_id).await.unwrap(), + account_id, + SyncCollection::Calendar, + ) + .await + .unwrap(); + let path = format!( + "{}{}", + resources.base_path, + resources + .paths + .iter() + .find(|v| v.parent_id.is_some()) + .unwrap() + .path + ); + + let ical = dav_client + .request("GET", &path, "") + .await + .with_status(StatusCode::OK) + .expect_body() + .lines() + .map(String::from) + .collect::>(); + let expected_ical = TEST_ICAL_1 + .lines() + .map(String::from) + .collect::>(); + assert_eq!(ical, expected_ical); + + // Clean up + account.destroy_all_calendars().await; + params.assert_is_empty().await; } + +pub fn test_jscalendar_1() -> Value { + json!({ + "duration": "PT1H", + "@type": "Event", + "description": "Go Steelers!", + "updated": "2006-02-06T00:11:02Z", + "timeZone": "US/Eastern", + "start": "2006-01-02T10:00:00", + "title": "Event #1", + "uid": "74855313FA803DA593CD579A@example.com" + }) +} + +pub fn test_jscalendar_2() -> Value { + json!({ + "title": "Event #2", + "duration": "PT1H", + "updated": "2006-02-06T00:11:21Z", + "recurrenceRule": { + "frequency": "daily", + "count": 5 + }, + "start": "2006-01-02T12:00:00", + "uid": "00959BC664CA650E933C892C@example.com", + "@type": "Event", + "timeZone": "US/Eastern", + "recurrenceOverrides": { + "2006-01-04T12:00:00": { + "title": "Event #2 bis", + "start": "2006-01-04T14:00:00", + "updated": "2006-02-06T00:11:21Z", + "duration": "PT1H" + }, + "2006-01-06T12:00:00": { + "title": "Event #2 bis bis", + "start": "2006-01-06T14:00:00", + "updated": "2006-02-06T00:11:21Z", + "duration": "PT1H" + } + } + }) +} + +pub fn test_jscalendar_3() -> Value { + json!({ + "duration": "PT1H", + "organizerCalendarAddress": "mailto:cyrus@example.com", + "@type": "Event", + "start": "2006-01-04T10:00:00", + "status": "tentative", + "uid": "DC6C50A017428C5216A2F1CD@example.com", + "sequence": 1, + "participants": { + "3f5bc8c0-c722-5345-b7d9-5a899db08a30": { + "calendarAddress": "mailto:cyrus@example.com", + "@type": "Participant", + "roles": { + "attendee": true, + "chair": true + }, + "participationStatus": "accepted" + }, + "ec5e7db5-22a3-5ed5-89bf-c8894ab86805": { + "calendarAddress": "mailto:lisa@example.com", + "@type": "Participant", + "roles": { + "attendee": true + }, + "participationStatus": "needs-action" + } + }, + "title": "Event #3", + "updated": "2006-02-06T00:12:20Z", + "timeZone": "US/Eastern" + }) +} + +const TEST_ICAL_1: &str = r#"BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART;TZID=US/Eastern:20060102T100000 +UID:74855313FA803DA593CD579A@example.com +DURATION:PT1H +SUMMARY:Event one +DTSTAMP:20060206T001102Z +CATEGORIES:work +END:VEVENT +END:VCALENDAR +"#; diff --git a/tests/src/jmap/calendar/mod.rs b/tests/src/jmap/calendar/mod.rs index 1502b93b..62921ddd 100644 --- a/tests/src/jmap/calendar/mod.rs +++ b/tests/src/jmap/calendar/mod.rs @@ -5,9 +5,7 @@ */ pub mod acl; -pub mod availability; pub mod calendars; pub mod event; pub mod identity; pub mod notification; -pub mod principal; diff --git a/tests/src/jmap/contacts/contact.rs b/tests/src/jmap/contacts/contact.rs index 18203525..294463ac 100644 --- a/tests/src/jmap/contacts/contact.rs +++ b/tests/src/jmap/contacts/contact.rs @@ -49,13 +49,25 @@ pub async fn test(params: &mut JMAPTest) { .to_string(); // Create test contacts + let sarah_contact = test_jscontact_1().with_property( + JSContactProperty::::AddressBookIds, + [book1_id.as_str()].into_jmap_set(), + ); + let carlos_contact = test_jscontact_2().with_property( + JSContactProperty::::AddressBookIds, + [book2_id.as_str()].into_jmap_set(), + ); + let acme_contact = test_jscontact_3().with_property( + JSContactProperty::::AddressBookIds, + [book1_id.as_str(), book2_id.as_str()].into_jmap_set(), + ); let response = account .jmap_create( MethodObject::ContactCard, [ - test_jscontact_1([book1_id.as_str()]), - test_jscontact_2([book2_id.as_str()]), - test_jscontact_3([book1_id.as_str(), book2_id.as_str()]), + sarah_contact.clone(), + carlos_contact.clone(), + acme_contact.clone(), ], ) .await; @@ -83,97 +95,18 @@ pub async fn test(params: &mut JMAPTest) { let response = account .jmap_get( MethodObject::ContactCard, - [ - JSContactProperty::::Id, - JSContactProperty::AddressBookIds, - JSContactProperty::Name, - ], + Vec::<&str>::new(), [&sarah_contact_id, &carlos_contact_id, &acme_contact_id], ) .await; - - assert_eq!( - response.list()[0], - json!({ - "id": &sarah_contact_id, - "name": { - "full": "Sarah Johnson", - "components": [ - { - "kind": "surname", - "value": "Johnson" - }, - { - "kind": "given", - "value": "Sarah" - }, - { - "kind": "given2", - "value": "Marie" - }, - { - "kind": "title", - "value": "Dr." - }, - { - "kind": "credential", - "value": "Ph.D." - } - ], - "isOrdered": true - }, - "addressBookIds": { - &book1_id: true - }, - }) + response.list()[0].assert_is_equal( + sarah_contact.with_property(JSContactProperty::::Id, sarah_contact_id.as_str()), ); - assert_eq!( - response.list()[1], - json!({ - "id": &carlos_contact_id, - "name": { - "components": [ - { - "kind": "surname", - "value": "Rodriguez-Martinez" - }, - { - "kind": "given", - "value": "Carlos" - }, - { - "kind": "given2", - "value": "Alberto" - }, - { - "kind": "title", - "value": "Mr." - }, - { - "kind": "credential", - "value": "Jr." - } - ], - "isOrdered": true, - "full": "Carlos Rodriguez-Martinez" - }, - "addressBookIds": { - &book2_id: true - }, - }) + response.list()[1].assert_is_equal( + carlos_contact.with_property(JSContactProperty::::Id, carlos_contact_id.as_str()), ); - assert_eq!( - response.list()[2], - json!({ - "id": acme_contact_id, - "addressBookIds": { - &book1_id: true, - &book2_id: true - }, - "name": { - "full": "Acme Business Solutions Ltd." - }, - }) + response.list()[2].assert_is_equal( + acme_contact.with_property(JSContactProperty::::Id, acme_contact_id.as_str()), ); // Creating a contact without address book should fail @@ -390,6 +323,7 @@ pub async fn test(params: &mut JMAPTest) { ("email", "sarah.johnson@example.com"), ], ["created"], + Vec::<(&str, &str)>::new(), ) .await .ids() @@ -535,11 +469,10 @@ END:VCARD"# params.assert_is_empty().await; } -fn test_jscontact_1(ids: impl IntoJmapSet) -> Value { +fn test_jscontact_1() -> Value { json!({ "uid": "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6", "@type": "Card", - "addressBookIds": ids.into_jmap_set(), "preferredLanguages": { "k1": { "language": "en", @@ -579,7 +512,8 @@ fn test_jscontact_1(ids: impl IntoJmapSet) -> Value { "kind": "credential", "value": "Ph.D." } - ] + ], + "isOrdered": true }, "cryptoKeys": { "k1": { @@ -713,7 +647,8 @@ fn test_jscontact_1(ids: impl IntoJmapSet) -> Value { } ], "timeZone": "Etc/GMT+5", - "coordinates": "40.7128;-74.0060" + "coordinates": "40.7128;-74.0060", + "isOrdered": true }, "k2": { "contexts": { @@ -742,7 +677,8 @@ fn test_jscontact_1(ids: impl IntoJmapSet) -> Value { "kind": "country", "value": "USA" } - ] + ], + "isOrdered": true } }, "titles": { @@ -770,9 +706,8 @@ fn test_jscontact_1(ids: impl IntoJmapSet) -> Value { }) } -fn test_jscontact_2(ids: impl IntoJmapSet) -> Value { +fn test_jscontact_2() -> Value { json!({ - "addressBookIds": ids.into_jmap_set(), "phones": { "k1": { "number": "+34-611-234-567", @@ -861,7 +796,8 @@ fn test_jscontact_2(ids: impl IntoJmapSet) -> Value { "value": "Jr." } ], - "full": "Carlos Rodriguez-Martinez" + "full": "Carlos Rodriguez-Martinez", + "isOrdered": true }, "nicknames": { "k1": { @@ -991,7 +927,8 @@ fn test_jscontact_2(ids: impl IntoJmapSet) -> Value { } ], "timeZone": "Etc/GMT-1", - "coordinates": "40.4168;-3.7038" + "coordinates": "40.4168;-3.7038", + "isOrdered": true }, "k2": { "contexts": { @@ -1016,7 +953,8 @@ fn test_jscontact_2(ids: impl IntoJmapSet) -> Value { "kind": "country", "value": "Spain" } - ] + ], + "isOrdered": true } }, "organizations": { @@ -1032,9 +970,8 @@ fn test_jscontact_2(ids: impl IntoJmapSet) -> Value { }) } -fn test_jscontact_3(ids: impl IntoJmapSet) -> Value { +fn test_jscontact_3() -> Value { json!({ - "addressBookIds": ids.into_jmap_set(), "kind": "org", "organizations": { "k1": { @@ -1098,8 +1035,7 @@ fn test_jscontact_3(ids: impl IntoJmapSet) -> Value { } }, "name": { - "full": "Acme Business Solutions Ltd.", - "components": [] + "full": "Acme Business Solutions Ltd." }, "notes": { "k1": { @@ -1180,7 +1116,8 @@ fn test_jscontact_3(ids: impl IntoJmapSet) -> Value { } ], "timeZone": "Etc/UTC", - "coordinates": "51.5074;-0.1278" + "coordinates": "51.5074;-0.1278", + "isOrdered": true }, "k2": { "contexts": { @@ -1204,7 +1141,8 @@ fn test_jscontact_3(ids: impl IntoJmapSet) -> Value { "kind": "country", "value": "United Kingdom" } - ] + ], + "isOrdered": true } }, "updated": "2023-04-15T15:30:00Z", diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 17d9fcc3..116b3eac 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -65,6 +65,7 @@ pub mod contacts; pub mod core; pub mod files; pub mod mail; +pub mod principal; pub mod server; #[tokio::test(flavor = "multi_thread")] @@ -106,12 +107,15 @@ async fn jmap_tests() { server::purge::test(&mut params).await; server::enterprise::test(&mut params).await;*/ - //contacts::addressbook::test(&mut params).await; - //contacts::contact::test(&mut params).await; + /*contacts::addressbook::test(&mut params).await; + contacts::contact::test(&mut params).await; contacts::acl::test(&mut params).await; - //files::node::test(&mut params).await; - files::acl::test(&mut params).await; + files::node::test(&mut params).await; + files::acl::test(&mut params).await;*/ + + //calendar::calendars::test(&mut params).await; + calendar::event::test(&mut params).await; if delete { params.temp_dir.delete(); @@ -564,6 +568,7 @@ impl Account { object: impl Display, filter: impl IntoIterator)>, sort_by: impl IntoIterator, + arguments: impl IntoIterator)>, ) -> JmapResponse { let filter = filter .into_iter() @@ -577,15 +582,20 @@ impl Account { }) }) .collect::>(); - self.jmap_method_calls(json!([[ - format!("{object}/query"), - { - "filter": filter, - "sort": sort_by - }, - "0" - ]])) - .await + let arguments = [ + ("filter".to_string(), Value::Object(filter)), + ("sort".to_string(), Value::Array(sort_by)), + ] + .into_iter() + .chain( + arguments + .into_iter() + .map(|(k, v)| (k.to_string(), v.into())), + ) + .collect::>(); + + self.jmap_method_calls(json!([[format!("{object}/query"), arguments, "0"]])) + .await } pub async fn jmap_create( @@ -798,6 +808,33 @@ impl Account { ])) .await; } + + pub async fn destroy_all_calendars(&self) { + self.jmap_method_calls(json!([[ + "Calendar/get", + { + "ids" : (), + "properties" : [ + "id" + ] + }, + "R1" + ], + [ + "Calendar/set", + { + "#destroy" : { + "resultOf": "R1", + "name": "Calendar/get", + "path": "/list/*/id" + }, + "onDestroyRemoveEvents" : true + }, + "R2" + ] + ])) + .await; + } } impl JmapResponse { @@ -837,6 +874,12 @@ impl JmapResponse { .unwrap_or_else(|| panic!("Missing method response in response: {self:?}")) } + pub fn list_array(&self) -> &Value { + self.0 + .pointer("/methodResponses/0/1/list") + .unwrap_or_else(|| panic!("Missing list in response: {self:?}")) + } + pub fn list(&self) -> &[Value] { self.0 .pointer("/methodResponses/0/1/list") @@ -933,6 +976,8 @@ pub trait JmapUtils { self.text_field("description") } + fn with_property(self, field: impl Display, value: impl Into) -> Self; + fn text_field(&self, field: &str) -> &str; fn assert_is_equal(&self, other: Value); @@ -953,6 +998,14 @@ impl JmapUtils for Value { ); } } + fn with_property(mut self, field: impl Display, value: impl Into) -> Self { + if let Value::Object(map) = &mut self { + map.insert(field.to_string(), value.into()); + } else { + panic!("Not an object: {self:?}"); + } + self + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/tests/src/jmap/calendar/availability.rs b/tests/src/jmap/principal/availability.rs similarity index 100% rename from tests/src/jmap/calendar/availability.rs rename to tests/src/jmap/principal/availability.rs diff --git a/tests/src/jmap/calendar/principal.rs b/tests/src/jmap/principal/get.rs similarity index 100% rename from tests/src/jmap/calendar/principal.rs rename to tests/src/jmap/principal/get.rs diff --git a/tests/src/jmap/principal/mod.rs b/tests/src/jmap/principal/mod.rs new file mode 100644 index 00000000..6077706c --- /dev/null +++ b/tests/src/jmap/principal/mod.rs @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod availability; +pub mod get;