Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+711
View File
@@ -0,0 +1,711 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::{jmap::JmapUtils, server::TestServer};
use calcard::jscalendar::JSCalendarProperty;
use jmap_proto::{
object::{calendar::CalendarProperty, share_notification::ShareNotificationProperty},
request::method::MethodObject,
};
use serde_json::json;
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running Calendar ACL tests...");
let john = test.account("[email protected]");
let jane = test.account("[email protected]");
let john_id = john.id_string().to_string();
let jane_id = jane.id_string().to_string();
// Create test calendars
let response = john
.jmap_create(
MethodObject::Calendar,
[json!({
"name": "Test #1",
})],
Vec::<(&str, &str)>::new(),
)
.await;
let john_calendar_id = response.created(0).id().to_string();
let john_event_id = john
.jmap_create(
MethodObject::CalendarEvent,
[json!({
"@type": "Event",
"uid": "a8df6573-0474-496d-8496-033ad45d7fea",
"updated": "2020-01-02T18:23:04Z",
"title": "John's Simple Event",
"start": "2020-01-15T13:00:00",
"timeZone": "America/New_York",
"duration": "PT1H",
"calendarIds": {
&john_calendar_id: true
},
})],
Vec::<(&str, &str)>::new(),
)
.await
.created(0)
.id()
.to_string();
let response = jane
.jmap_create(
MethodObject::Calendar,
[json!({
"name": "Test #1",
})],
Vec::<(&str, &str)>::new(),
)
.await;
let jane_calendar_id = response.created(0).id().to_string();
let jane_event_id = jane
.jmap_create(
MethodObject::CalendarEvent,
[json!({
"uid": "a8df6575-0474-496d-8496-033ad45d7fea",
"updated": "2020-01-02T18:23:04Z",
"title": "Jane's Simple Event",
"start": "2020-01-15T13:00:00",
"timeZone": "America/New_York",
"duration": "PT1H",
"calendarIds": {
&jane_calendar_id: true
},
})],
Vec::<(&str, &str)>::new(),
)
.await
.created(0)
.id()
.to_string();
// Verify myRights
john.jmap_get(
MethodObject::Calendar,
[
CalendarProperty::Id,
CalendarProperty::Name,
CalendarProperty::MyRights,
CalendarProperty::ShareWith,
],
[john_calendar_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_calendar_id,
"name": "Test #1",
"myRights": {
"mayReadItems": true,
"mayWriteAll": true,
"mayDelete": true,
"mayShare": true,
"mayWriteOwn": true,
"mayReadFreeBusy": true,
"mayUpdatePrivate": true,
"mayRSVP": true
},
"shareWith": {}
}));
// Obtain share notifications
let mut jane_share_change_id = jane
.jmap_get(
MethodObject::ShareNotification,
Vec::<&str>::new(),
Vec::<&str>::new(),
)
.await
.state()
.to_string();
// Make sure Jane has no access
assert_eq!(
jane.jmap_get_account(
john,
MethodObject::Calendar,
Vec::<&str>::new(),
[john_calendar_id.as_str()],
)
.await
.method_response()
.typ(),
"forbidden"
);
// Share calendar with Jane
john.jmap_update(
MethodObject::Calendar,
[(
&john_calendar_id,
json!({
"shareWith": {
&jane_id : {
"mayReadItems": true,
}
}
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&john_calendar_id);
john.jmap_get(
MethodObject::Calendar,
[
CalendarProperty::Id,
CalendarProperty::Name,
CalendarProperty::ShareWith,
],
[john_calendar_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_calendar_id,
"name": "Test #1",
"shareWith": {
&jane_id : {
"mayReadItems": true,
"mayWriteAll": false,
"mayDelete": false,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
}
}
}));
// Verify Jane can access the event
jane.jmap_get_account(
john,
MethodObject::Calendar,
[
CalendarProperty::Id,
CalendarProperty::Name,
CalendarProperty::MyRights,
],
[john_calendar_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_calendar_id,
"name": "Test #1",
"myRights": {
"mayReadItems": true,
"mayWriteAll": false,
"mayDelete": false,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
}
}));
jane.jmap_get_account(
john,
MethodObject::CalendarEvent,
[JSCalendarProperty::<Id>::Id, JSCalendarProperty::Title],
[john_event_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_event_id,
"title": "John's Simple Event",
}));
// Verify Jane received a share notification
let response = jane
.jmap_changes(MethodObject::ShareNotification, &jane_share_change_id)
.await;
jane_share_change_id = response.new_state().to_string();
let changes = response.changes().collect::<Vec<_>>();
assert_eq!(changes.len(), 1);
let share_id = changes[0].as_created();
jane.jmap_get(
MethodObject::ShareNotification,
[
ShareNotificationProperty::Id,
ShareNotificationProperty::ChangedBy,
ShareNotificationProperty::ObjectType,
ShareNotificationProperty::ObjectAccountId,
ShareNotificationProperty::ObjectId,
ShareNotificationProperty::OldRights,
ShareNotificationProperty::NewRights,
ShareNotificationProperty::Name,
],
[share_id],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": &share_id,
"changedBy": {
"principalId": &john_id,
"name": "John Doe",
"email": "[email protected]"
},
"objectType": "Calendar",
"objectAccountId": &john_id,
"objectId": &john_calendar_id,
"oldRights": {
"mayReadItems": false,
"mayWriteAll": false,
"mayDelete": false,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
},
"newRights": {
"mayReadItems": true,
"mayWriteAll": false,
"mayDelete": false,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
},
"name": null
}));
// Updating and deleting should fail
assert_eq!(
jane.jmap_update_account(
john,
MethodObject::Calendar,
[(&john_calendar_id, json!({}))],
Vec::<(&str, &str)>::new(),
)
.await
.not_updated(&john_calendar_id)
.description(),
"You are not allowed to modify this calendar."
);
assert_eq!(
jane.jmap_destroy_account(
john,
MethodObject::Calendar,
[&john_calendar_id],
Vec::<(&str, &str)>::new(),
)
.await
.not_destroyed(&john_calendar_id)
.description(),
"You are not allowed to delete this calendar."
);
assert!(
jane.jmap_update_account(
john,
MethodObject::CalendarEvent,
[(&john_event_id, json!({}))],
Vec::<(&str, &str)>::new(),
)
.await
.not_updated(&john_event_id)
.description()
.contains("You are not allowed to modify calendar"),
);
assert!(
jane.jmap_destroy_account(
john,
MethodObject::CalendarEvent,
[&john_event_id],
Vec::<(&str, &str)>::new(),
)
.await
.not_destroyed(&john_event_id)
.description()
.contains("You are not allowed to remove events from calendar"),
);
// Grant Jane write access
john.jmap_update(
MethodObject::Calendar,
[(
&john_calendar_id,
json!({
format!("shareWith/{jane_id}/mayWriteAll"): true,
format!("shareWith/{jane_id}/mayDelete"): true,
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&john_calendar_id);
jane.jmap_get_account(
john,
MethodObject::Calendar,
[
CalendarProperty::Id,
CalendarProperty::Name,
CalendarProperty::MyRights,
],
[john_calendar_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_calendar_id,
"name": "Test #1",
"myRights": {
"mayReadItems": true,
"mayWriteAll": true,
"mayDelete": true,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
}
}));
// Verify Jane received a share notification with the updated rights
let response = jane
.jmap_changes(MethodObject::ShareNotification, &jane_share_change_id)
.await;
jane_share_change_id = response.new_state().to_string();
let changes = response.changes().collect::<Vec<_>>();
assert_eq!(changes.len(), 1);
let share_id = changes[0].as_created();
jane.jmap_get(
MethodObject::ShareNotification,
[
ShareNotificationProperty::Id,
ShareNotificationProperty::ChangedBy,
ShareNotificationProperty::ObjectType,
ShareNotificationProperty::ObjectAccountId,
ShareNotificationProperty::ObjectId,
ShareNotificationProperty::OldRights,
ShareNotificationProperty::NewRights,
ShareNotificationProperty::Name,
],
[share_id],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": &share_id,
"changedBy": {
"principalId": &john_id,
"name": "John Doe",
"email": "[email protected]"
},
"objectType": "Calendar",
"objectAccountId": &john_id,
"objectId": &john_calendar_id,
"oldRights": {
"mayReadItems": true,
"mayWriteAll": false,
"mayDelete": false,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
},
"newRights": {
"mayReadItems": true,
"mayWriteAll": true,
"mayDelete": true,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
},
"name": null
}));
// Creating a root folder should fail
assert_eq!(
jane.jmap_create_account(
john,
MethodObject::Calendar,
[json!({
"name": "A new shared calendar",
})],
Vec::<(&str, &str)>::new()
)
.await
.not_created(0)
.description(),
"Cannot create calendars in a shared account."
);
// Copy Jane's event into John's calendar
let john_copied_event_id = jane
.jmap_copy(
jane,
john,
MethodObject::CalendarEvent,
[(
&jane_event_id,
json!({
"calendarIds": {
&john_calendar_id: true
}
}),
)],
false,
)
.await
.copied(&jane_event_id)
.id()
.to_string();
jane.jmap_get_account(
john,
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::CalendarIds,
JSCalendarProperty::Title,
],
[john_copied_event_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_copied_event_id,
"title": "Jane's Simple Event",
"calendarIds": {
&john_calendar_id: true
}
}));
// Destroy the copied event
assert_eq!(
jane.jmap_destroy_account(
john,
MethodObject::CalendarEvent,
[john_copied_event_id.as_str()],
Vec::<(&str, &str)>::new(),
)
.await
.destroyed()
.collect::<Vec<_>>(),
[&john_copied_event_id]
);
// Update John's event
jane.jmap_update_account(
john,
MethodObject::CalendarEvent,
[(
&john_event_id,
json!({
"title": "John's Updated Event",
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&john_event_id);
jane.jmap_get_account(
john,
MethodObject::CalendarEvent,
[JSCalendarProperty::<Id>::Id, JSCalendarProperty::Title],
[john_event_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_event_id,
"title": "John's Updated Event",
}));
// Update John's calendar name
jane.jmap_update_account(
john,
MethodObject::Calendar,
[(
&john_calendar_id,
json!({
"name": "Jane's version of John's Calendar",
"description": "This is John's calendar, but Jane can edit it now"
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&john_calendar_id);
jane.jmap_get_account(
john,
MethodObject::Calendar,
[
CalendarProperty::Id,
CalendarProperty::Name,
CalendarProperty::Description,
],
[john_calendar_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_calendar_id,
"name": "Jane's version of John's Calendar",
"description": "This is John's calendar, but Jane can edit it now"
}));
// John should still see the old name
john.jmap_get(
MethodObject::Calendar,
[
CalendarProperty::Id,
CalendarProperty::Name,
CalendarProperty::Description,
],
[john_calendar_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_calendar_id,
"name": "Test #1",
"description": null
}));
// Revoke Jane's access
john.jmap_update(
MethodObject::Calendar,
[(
&john_calendar_id,
json!({
format!("shareWith/{jane_id}"): ()
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&john_calendar_id);
john.jmap_get(
MethodObject::Calendar,
[
CalendarProperty::Id,
CalendarProperty::Name,
CalendarProperty::ShareWith,
],
[john_calendar_id.as_str()],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": john_calendar_id,
"name": "Test #1",
"shareWith": {}
}));
// Verify Jane can no longer access the calendar or its events
assert_eq!(
jane.jmap_get_account(
john,
MethodObject::Calendar,
Vec::<&str>::new(),
[john_calendar_id.as_str()],
)
.await
.method_response()
.typ(),
"forbidden"
);
// Verify Jane received a share notification with the updated rights
let response = jane
.jmap_changes(MethodObject::ShareNotification, &jane_share_change_id)
.await;
let changes = response.changes().collect::<Vec<_>>();
assert_eq!(changes.len(), 1);
let share_id = changes[0].as_created();
jane.jmap_get(
MethodObject::ShareNotification,
[
ShareNotificationProperty::Id,
ShareNotificationProperty::ChangedBy,
ShareNotificationProperty::ObjectType,
ShareNotificationProperty::ObjectAccountId,
ShareNotificationProperty::ObjectId,
ShareNotificationProperty::OldRights,
ShareNotificationProperty::NewRights,
ShareNotificationProperty::Name,
],
[share_id],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": &share_id,
"changedBy": {
"principalId": &john_id,
"name": "John Doe",
"email": "[email protected]"
},
"objectType": "Calendar",
"objectAccountId": &john_id,
"objectId": &john_calendar_id,
"oldRights": {
"mayReadItems": true,
"mayWriteAll": true,
"mayDelete": true,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
},
"newRights": {
"mayReadItems": false,
"mayWriteAll": false,
"mayDelete": false,
"mayShare": false,
"mayWriteOwn": false,
"mayReadFreeBusy": false,
"mayUpdatePrivate": false,
"mayRSVP": false
},
"name": null
}));
// Grant Jane delete access once again
john.jmap_update(
MethodObject::Calendar,
[(
&john_calendar_id,
json!({
format!("shareWith/{jane_id}/mayReadItems"): true,
format!("shareWith/{jane_id}/mayDelete"): true,
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&john_calendar_id);
// Verify Jane can delete the calendar
assert_eq!(
jane.jmap_destroy_account(
john,
MethodObject::Calendar,
[john_calendar_id.as_str()],
[("onDestroyRemoveEvents", true)],
)
.await
.destroyed()
.collect::<Vec<_>>(),
[john_calendar_id.as_str()]
);
// Destroy all mailboxes
john.destroy_all_calendars().await;
jane.destroy_all_calendars().await;
test.assert_is_empty().await;
}
+175
View File
@@ -0,0 +1,175 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::{
jmap::{IntoJmapSet, JmapUtils},
server::TestServer,
};
use futures::StreamExt;
use jmap_client::{
CalendarAlert, PushObject, client_ws::WebSocketMessage, event_source::PushNotification,
};
use jmap_proto::request::method::MethodObject;
use mail_parser::DateTime;
use serde_json::json;
use std::time::Instant;
use store::write::now;
use tokio::sync::mpsc;
pub async fn test(test: &TestServer) {
println!("Running Calendar Alarm tests...");
let account = test.account("[email protected]");
let account_id = account.id_string();
let client = account.jmap_client().await;
let client_ws = account.jmap_client().await;
// Create test calendar
let response = account
.jmap_create(
MethodObject::Calendar,
[json!({
"name": "Alarming Calendar",
})],
Vec::<(&str, &str)>::new(),
)
.await;
let calendar_id = response.created(0).id().to_string();
// Connect to EventSource
let (event_tx, mut event_rx) = mpsc::channel::<PushNotification>(100);
let mut notifications = client
.event_source(None::<Vec<_>>, false, 1.into(), None)
.await
.unwrap();
tokio::spawn(async move {
while let Some(notification) = notifications.next().await {
if let Err(_err) = event_tx.send(notification.unwrap()).await {
break;
}
}
});
// Connect to WebSocket
let mut ws_stream = client_ws.connect_ws().await.unwrap();
let (stream_tx, mut stream_rx) = mpsc::channel::<WebSocketMessage>(100);
tokio::spawn(async move {
while let Some(change) = ws_stream.next().await {
if stream_tx.send(change.unwrap()).await.is_err() {
break;
}
}
});
client_ws
.enable_push_ws(None::<Vec<_>>, None::<&str>)
.await
.unwrap();
// Create test event
let response = account
.jmap_create(
MethodObject::CalendarEvent,
[json!({
"@type": "Event",
"calendarIds": ([calendar_id.as_str()].into_jmap_set()),
"description": "What mirror where?!",
"timeZone": "Etc/UTC",
"start": DateTime::from_timestamp(now() as i64 + 5)
.to_rfc3339().trim_end_matches("Z").to_string(),
"title": "See the pretty girl in that mirror there",
"alerts": {
"k1": {
"@type": "Alert",
"trigger": {
"@type": "OffsetTrigger",
"offset": "-PT2S"
},
"action": "display"
},
"k2": {
"trigger": {
"@type": "OffsetTrigger",
"offset": "-PT4S"
},
"action": "display",
"@type": "Alert"
}
},
"locations": {
"0b7168ae-ed3e-5eae-9540-89ba3a469b16": {
"name": "West Side",
"@type": "Location"
}
},
"uid": "2371c2d9-a136-43b0-bba3-f6ab249ad46e",
"duration": "P1D"
})],
Vec::<(&str, &str)>::new(),
)
.await;
let event_id = response.created(0).id().to_string();
// Wait for alarm notifications
let start = Instant::now();
let mut ws_events = Vec::new();
let mut es_events = Vec::new();
while start.elapsed().as_secs() < 7 && (ws_events.len() < 2 || es_events.len() < 2) {
tokio::select! {
Some(notification) = event_rx.recv() => {
if let PushNotification::CalendarAlert(alert) = notification {
es_events.push(alert);
}
}
Some(message) = stream_rx.recv() => {
match message {
WebSocketMessage::PushNotification(PushObject::CalendarAlert(alert)) => {
ws_events.push(alert);
}
WebSocketMessage::PushNotification(PushObject::Group {entries} ) => {
ws_events.extend(entries.into_iter().filter_map(|entry| {
if let PushObject::CalendarAlert(alert) = entry {
Some(alert)
} else {
None
}
}));
}
_ => {}
}
}
_ = tokio::time::sleep(std::time::Duration::from_secs(6)) => {
break;
}
}
}
let expected_alerts = vec![
CalendarAlert {
account_id: account_id.to_string(),
calendar_event_id: event_id.clone(),
uid: "2371c2d9-a136-43b0-bba3-f6ab249ad46e".to_string(),
recurrence_id: None,
alert_id: "k2".to_string(),
},
CalendarAlert {
account_id: account_id.to_string(),
calendar_event_id: event_id.clone(),
uid: "2371c2d9-a136-43b0-bba3-f6ab249ad46e".to_string(),
recurrence_id: None,
alert_id: "k1".to_string(),
},
];
assert_eq!(
es_events, expected_alerts,
"EventSource alarms do not match"
);
assert_eq!(ws_events, expected_alerts, "WebSocket alarms do not match");
// Cleanup
account.destroy_all_calendars().await;
test.assert_is_empty().await;
}
+377
View File
@@ -0,0 +1,377 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::{
jmap::{ChangeType, JmapUtils},
server::TestServer,
};
use jmap_proto::{object::calendar::CalendarProperty, request::method::MethodObject};
use serde_json::json;
pub async fn test(test: &TestServer) {
println!("Running Calendar tests...");
let account = test.account("[email protected]");
// 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 ([email protected])",
"description": null,
"sortOrder": 0,
"isSubscribed": true,
"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"
}
}
}
})],
Vec::<(&str, &str)>::new(),
)
.await
.created(0)
.id()
.to_string();
// Validate changes
assert_eq!(
account
.jmap_changes(MethodObject::Calendar, change_id)
.await
.changes()
.collect::<Vec<_>>(),
[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 ([email protected])",
"description": (),
"sortOrder": 0,
"isSubscribed": true,
"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"
})],
Vec::<(&str, &str)>::new(),
)
.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<_>>(),
vec![&calendar_id]
);
// Destroy all mailboxes
account.destroy_all_calendars().await;
test.assert_is_empty().await;
}
File diff suppressed because it is too large Load Diff
+151
View File
@@ -0,0 +1,151 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::{jmap::JmapUtils, server::TestServer};
use jmap_proto::{
object::participant_identity::ParticipantIdentityProperty, request::method::MethodObject,
};
use serde_json::json;
use store::write::BatchBuilder;
use types::{collection::Collection, field::PrincipalField};
pub async fn test(test: &TestServer) {
println!("Running Participant Identity tests...");
let account = test.account("[email protected]");
// Obtain all identities
let response = account
.jmap_get(
MethodObject::ParticipantIdentity,
[
ParticipantIdentityProperty::Id,
ParticipantIdentityProperty::Name,
ParticipantIdentityProperty::CalendarAddress,
ParticipantIdentityProperty::IsDefault,
],
Vec::<&str>::new(),
)
.await;
response.list_array().assert_is_equal(json!([
{
"id": "a",
"name": "John Doe",
"calendarAddress": "mailto:[email protected]",
"isDefault": true
},
{
"id": "b",
"name": "John Doe",
"calendarAddress": "mailto:[email protected]",
"isDefault": false
}
]));
// Destroy identity b
let response = account
.jmap_destroy(
MethodObject::ParticipantIdentity,
["b"],
Vec::<(&str, &str)>::new(),
)
.await;
assert_eq!(response.destroyed().next(), Some("b"));
let response = account
.jmap_get(
MethodObject::ParticipantIdentity,
[
ParticipantIdentityProperty::Id,
ParticipantIdentityProperty::Name,
ParticipantIdentityProperty::CalendarAddress,
ParticipantIdentityProperty::IsDefault,
],
Vec::<&str>::new(),
)
.await;
response.list_array().assert_is_equal(json!([
{
"id": "a",
"name": "John Doe",
"calendarAddress": "mailto:[email protected]",
"isDefault": true
}
]));
// Creating a new identity with an unauthorized calendar address should fail
let response = account
.jmap_create(
MethodObject::ParticipantIdentity,
[
json!({
"name": "Work",
"calendarAddress": "mailto:[email protected]"
}),
json!({
"name": "Work",
"calendarAddress": "[email protected]"
}),
],
[("onSuccessSetIsDefault", "#i0")],
)
.await;
assert_eq!(
response.not_created(0).description(),
"Calendar address not configured for this account."
);
assert_eq!(
response.not_created(1).description(),
"Calendar address not configured for this account."
);
// Create a new identity and set it as default
let response = account
.jmap_create(
MethodObject::ParticipantIdentity,
[json!({
"name": "Johnny B Goode",
"calendarAddress": "mailto:[email protected]"
})],
[("onSuccessSetIsDefault", "#i0")],
)
.await;
response.created(0);
let response = account
.jmap_get(
MethodObject::ParticipantIdentity,
[
ParticipantIdentityProperty::Id,
ParticipantIdentityProperty::Name,
ParticipantIdentityProperty::CalendarAddress,
ParticipantIdentityProperty::IsDefault,
],
Vec::<&str>::new(),
)
.await;
response.list_array().assert_is_equal(json!([
{
"id": "a",
"name": "John Doe",
"calendarAddress": "mailto:[email protected]",
"isDefault": false
},
{
"id": "b",
"name": "Johnny B Goode",
"calendarAddress": "mailto:[email protected]",
"isDefault": true
}
]));
// Cleanup
let mut batch = BatchBuilder::new();
batch
.with_account_id(account.id().document_id())
.with_collection(Collection::Principal)
.with_document(0)
.clear(PrincipalField::ParticipantIdentities);
test.server.commit_batch(batch).await.unwrap();
test.assert_is_empty().await;
}
+991
View File
@@ -0,0 +1,991 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
jmap::calendar::event::assert_eq_ignoring_updated,
utils::{account::Account, jmap::JmapUtils, server::TestServer},
};
use groupware::cache::GroupwareCache;
use hyper::StatusCode;
use jmap_proto::request::method::MethodObject;
use serde_json::{Value, json};
use std::str::FromStr;
use types::{collection::SyncCollection, id::Id};
pub async fn test(test: &TestServer) {
println!("Running Calendar Event instance tests...");
let account = test.account("[email protected]");
let calendar_id = account
.jmap_create(
MethodObject::Calendar,
[json!({
"name": "Recurring events",
"timeZone": "US/Eastern",
})],
Vec::<(&str, &str)>::new(),
)
.await
.created(0)
.id()
.to_string();
let response = account
.jmap_create(
MethodObject::CalendarEvent,
[
json!({
"@type": "Event",
"uid": "[email protected]",
"title": "Daily standup",
"start": "2007-03-05T12:00:00",
"duration": "PT1H",
"timeZone": "US/Eastern",
"updated": "2007-02-06T00:11:21Z",
"recurrenceRule": {
"frequency": "daily",
"count": 5
},
"locations": {
"loc1": {
"@type": "Location",
"name": "Room A"
}
},
"recurrenceOverrides": {
"2007-03-07T12:00:00": {
"title": "Moved standup",
"start": "2007-03-07T15:00:00",
"duration": "PT1H",
"updated": "2007-02-06T00:11:21Z"
}
},
"calendarIds": {
&calendar_id: true
}
}),
json!({
"@type": "Event",
"uid": "[email protected]",
"title": "One off",
"start": "2007-03-12T09:00:00",
"duration": "PT2H",
"timeZone": "US/Eastern",
"updated": "2007-02-06T00:11:21Z",
"calendarIds": {
&calendar_id: true
}
}),
],
Vec::<(&str, &str)>::new(),
)
.await;
let recurring_id = response.created(0).id().to_string();
let single_id = response.created(1).id().to_string();
test.wait_for_tasks().await;
// The overridden occurrence keeps its original recurrence id
let instances = expand_instances(account, &calendar_id).await;
assert_eq!(
starts(&instances),
[
"2007-03-05T12:00:00",
"2007-03-06T12:00:00",
"2007-03-07T15:00:00",
"2007-03-08T12:00:00",
"2007-03-09T12:00:00",
"2007-03-12T09:00:00"
]
);
assert_eq!(
instance(&instances, "2007-03-07T15:00:00").text_field("recurrenceId"),
"2007-03-07T12:00:00"
);
// Synthetic instances return null recurrence properties when requested
for start in [
"2007-03-05T12:00:00",
"2007-03-06T12:00:00",
"2007-03-07T15:00:00",
"2007-03-08T12:00:00",
"2007-03-09T12:00:00",
] {
let instance = instance(&instances, start);
for property in ["recurrenceRule", "recurrenceOverrides"] {
assert_eq!(
instance.get(property),
Some(&Value::Null),
"{property} on {start}: {instance:?}"
);
}
}
// Unknown instances are reported as not found
let unknown_id =
Id::from_parts(1000, Id::from_str(&recurring_id).unwrap().document_id()).to_string();
let response = account
.jmap_update(
MethodObject::CalendarEvent,
[(&unknown_id, json!({"title": "Nope"}))],
Vec::<(&str, &str)>::new(),
)
.await;
assert_eq!(response.not_updated(&unknown_id).typ(), "notFound");
// Updating an instance generated by the recurrence rule creates an override
let id = instance_id(&instances, "2007-03-06T12:00:00");
account
.jmap_update(
MethodObject::CalendarEvent,
[(
&id,
json!({
"title": "Standup with guests",
"locations/loc1/name": "Room B"
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&id);
test.wait_for_tasks().await;
let instances = expand_instances(account, &calendar_id).await;
let updated = instance(&instances, "2007-03-06T12:00:00");
assert_eq!(updated.text_field("title"), "Standup with guests");
assert_eq!(updated.text_field("duration"), "PT1H");
assert_eq!(
updated
.pointer("/locations/loc1/name")
.and_then(|v| v.as_str()),
Some("Room B")
);
assert_eq!(
instance(&instances, "2007-03-05T12:00:00").text_field("title"),
"Daily standup"
);
// Updating an occurrence that is already overridden patches the existing override
let id = instance_id(&instances, "2007-03-07T15:00:00");
account
.jmap_update(
MethodObject::CalendarEvent,
[(
&id,
json!({
"title": "Moved standup, renamed"
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&id);
test.wait_for_tasks().await;
assert_eq_ignoring_updated(
&base_event(account, &recurring_id).await,
json!({
"id": &recurring_id,
"title": "Daily standup",
"locations": {
"loc1": {
"@type": "Location",
"name": "Room A"
}
},
"recurrenceOverrides": {
"2007-03-06T12:00:00": {
"title": "Standup with guests",
"start": "2007-03-06T12:00:00",
"duration": "PT1H",
"locations": {
"loc1": {
"@type": "Location",
"name": "Room B"
}
}
},
"2007-03-07T12:00:00": {
"title": "Moved standup, renamed",
"start": "2007-03-07T15:00:00",
"duration": "PT1H"
}
}
}),
);
// A rejected instance is not written, even when another instance of the same event is
let instances = expand_instances(account, &calendar_id).await;
let good_id = instance_id(&instances, "2007-03-09T12:00:00");
let bad_id = instance_id(&instances, "2007-03-08T12:00:00");
let response = account
.jmap_update(
MethodObject::CalendarEvent,
[
(&good_id, json!({"title": "Applied"})),
(
&bad_id,
json!({"title": "Rejected", "utcStart": "2007-03-08T17:00:00Z"}),
),
],
Vec::<(&str, &str)>::new(),
)
.await;
response.updated(&good_id);
assert_eq!(response.not_updated(&bad_id).typ(), "invalidProperties");
test.wait_for_tasks().await;
let base = base_event(account, &recurring_id).await;
let mut overrides = base
.pointer("/recurrenceOverrides")
.and_then(|value| value.as_object())
.unwrap()
.keys()
.map(String::as_str)
.collect::<Vec<_>>();
overrides.sort_unstable();
assert_eq!(
overrides,
[
"2007-03-06T12:00:00",
"2007-03-07T12:00:00",
"2007-03-09T12:00:00"
]
);
// Destroying an instance removes just that occurrence
let instances = expand_instances(account, &calendar_id).await;
let id = instance_id(&instances, "2007-03-08T12:00:00");
assert_eq!(
account
.jmap_destroy(
MethodObject::CalendarEvent,
[&id],
Vec::<(&str, &str)>::new(),
)
.await
.destroyed()
.collect::<Vec<_>>(),
[id.as_str()]
);
test.wait_for_tasks().await;
assert_eq!(
starts(&expand_instances(account, &calendar_id).await),
[
"2007-03-05T12:00:00",
"2007-03-06T12:00:00",
"2007-03-07T15:00:00",
"2007-03-09T12:00:00",
"2007-03-12T09:00:00"
]
);
// Destroying an overridden instance does not bring back the original occurrence
let instances = expand_instances(account, &calendar_id).await;
let id = instance_id(&instances, "2007-03-07T15:00:00");
assert_eq!(
account
.jmap_destroy(
MethodObject::CalendarEvent,
[&id],
Vec::<(&str, &str)>::new(),
)
.await
.destroyed()
.collect::<Vec<_>>(),
[id.as_str()]
);
test.wait_for_tasks().await;
assert_eq!(
starts(&expand_instances(account, &calendar_id).await),
[
"2007-03-05T12:00:00",
"2007-03-06T12:00:00",
"2007-03-09T12:00:00",
"2007-03-12T09:00:00"
]
);
// Several instances of the same event may be changed in a single request
let instances = expand_instances(account, &calendar_id).await;
let update_id = instance_id(&instances, "2007-03-05T12:00:00");
let destroy_id = instance_id(&instances, "2007-03-09T12:00:00");
let response = account
.jmap_method_calls(json!([[
"CalendarEvent/set",
{
"accountId": account.id_string(),
"update": {
&update_id: {
"title": "First standup"
}
},
"destroy": [&destroy_id]
},
"0"
]]))
.await;
response.updated(&update_id);
assert_eq!(
response.destroyed().collect::<Vec<_>>(),
[destroy_id.as_str()]
);
test.wait_for_tasks().await;
let instances = expand_instances(account, &calendar_id).await;
assert_eq!(
starts(&instances),
[
"2007-03-05T12:00:00",
"2007-03-06T12:00:00",
"2007-03-12T09:00:00"
]
);
assert_eq!(
instance(&instances, "2007-03-05T12:00:00").text_field("title"),
"First standup"
);
// Excluding the occurrence the event starts on is allowed
let id = instance_id(&instances, "2007-03-05T12:00:00");
assert_eq!(
account
.jmap_destroy(
MethodObject::CalendarEvent,
[&id],
Vec::<(&str, &str)>::new(),
)
.await
.destroyed()
.collect::<Vec<_>>(),
[id.as_str()]
);
test.wait_for_tasks().await;
let instances = expand_instances(account, &calendar_id).await;
assert_eq!(
starts(&instances),
["2007-03-06T12:00:00", "2007-03-12T09:00:00"]
);
// A base event and its instances cannot be modified in the same request
let id = instance_id(&instances, "2007-03-06T12:00:00");
let response = account
.jmap_update(
MethodObject::CalendarEvent,
[
(&recurring_id, json!({"title": "Renamed"})),
(&id, json!({"title": "Renamed instance"})),
],
Vec::<(&str, &str)>::new(),
)
.await;
for id in [&recurring_id, &id] {
assert_eq!(response.not_updated(id).typ(), "invalidProperties");
assert_eq!(
response.not_updated(id).description(),
"A base event and its instances cannot be modified in the same request."
);
}
// Properties that are not per-occurrence are rejected
for property in [
json!({"calendarIds": {&calendar_id: true}}),
json!({"isDraft": true}),
json!({"utcStart": "2007-03-06T17:00:00Z"}),
json!({"utcEnd": "2007-03-06T18:00:00Z"}),
json!({"mayInviteSelf": true}),
json!({"useDefaultAlerts": true}),
] {
let response = account
.jmap_update(
MethodObject::CalendarEvent,
[(&id, property)],
Vec::<(&str, &str)>::new(),
)
.await;
assert_eq!(response.not_updated(&id).typ(), "invalidProperties");
assert_eq!(
response.not_updated(&id).description(),
"This property cannot be modified on a single occurrence."
);
}
// Properties an occurrence inherits from the base event are ignored, not rejected
account
.jmap_update(
MethodObject::CalendarEvent,
[(
&id,
json!({
"@type": "Event",
"title": "Ignoring inherited properties",
"uid": "[email protected]",
"recurrenceRule": {"frequency": "weekly"},
"privacy": "private",
"participants/xyz/calendarAddress": "mailto:[email protected]"
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&id);
test.wait_for_tasks().await;
let base = account
.jmap_get(
MethodObject::CalendarEvent,
["id", "uid", "recurrenceRule", "privacy", "participants"],
[&recurring_id],
)
.await;
let base = &base.list()[0];
assert_eq!(base.text_field("uid"), "[email protected]");
assert_eq!(
base.pointer("/recurrenceRule/frequency")
.and_then(|v| v.as_str()),
Some("daily")
);
assert_eq!(base.pointer("/privacy"), None);
assert_eq!(base.pointer("/participants"), None);
assert_eq!(
instance(
&expand_instances(account, &calendar_id).await,
"2007-03-06T12:00:00"
)
.text_field("title"),
"Ignoring inherited properties"
);
// Destroying an event that is also being updated through one of its instances fails
let response = account
.jmap_method_calls(json!([[
"CalendarEvent/set",
{
"accountId": account.id_string(),
"update": {
&id: {
"title": "Renamed instance"
}
},
"destroy": [&recurring_id]
},
"0"
]]))
.await;
assert_eq!(response.not_updated(&id).typ(), "willDestroy");
assert_eq!(
response.destroyed().collect::<Vec<_>>(),
[recurring_id.as_str()]
);
test.wait_for_tasks().await;
// A synthetic id of a non-recurring event refers to the event itself
let instances = expand_instances(account, &calendar_id).await;
let id = instance_id(&instances, "2007-03-12T09:00:00");
assert_ne!(id, single_id);
account
.jmap_update(
MethodObject::CalendarEvent,
[(&id, json!({"title": "One off, renamed"}))],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&id);
test.wait_for_tasks().await;
assert_eq_ignoring_updated(
&base_event(account, &single_id).await,
json!({
"id": &single_id,
"title": "One off, renamed"
}),
);
let instances = expand_instances(account, &calendar_id).await;
let id = instance_id(&instances, "2007-03-12T09:00:00");
assert_eq!(
account
.jmap_destroy(
MethodObject::CalendarEvent,
[&id],
Vec::<(&str, &str)>::new(),
)
.await
.destroyed()
.collect::<Vec<_>>(),
[id.as_str()]
);
test.wait_for_tasks().await;
assert!(expand_instances(account, &calendar_id).await.is_empty());
// Instances of floating all-day events keep their local time and duration
let all_day_id = account
.jmap_create(
MethodObject::CalendarEvent,
[json!({
"@type": "Event",
"uid": "[email protected]",
"title": "Spring break",
"start": "2007-04-02T00:00:00",
"duration": "P1D",
"showWithoutTime": true,
"updated": "2007-02-06T00:11:21Z",
"recurrenceRule": {
"frequency": "daily",
"count": 3
},
"calendarIds": {
&calendar_id: true
}
})],
Vec::<(&str, &str)>::new(),
)
.await
.created(0)
.id()
.to_string();
test.wait_for_tasks().await;
let instances = expand_instances(account, &calendar_id).await;
assert_eq!(
starts(&instances),
[
"2007-04-02T00:00:00",
"2007-04-03T00:00:00",
"2007-04-04T00:00:00"
]
);
let update_id = instance_id(&instances, "2007-04-03T00:00:00");
let destroy_id = instance_id(&instances, "2007-04-04T00:00:00");
let response = account
.jmap_method_calls(json!([[
"CalendarEvent/set",
{
"accountId": account.id_string(),
"update": {
&update_id: {
"title": "Spring break, day two"
}
},
"destroy": [&destroy_id]
},
"0"
]]))
.await;
response.updated(&update_id);
assert_eq!(
response.destroyed().collect::<Vec<_>>(),
[destroy_id.as_str()]
);
test.wait_for_tasks().await;
let instances = expand_instances(account, &calendar_id).await;
assert_eq!(
starts(&instances),
["2007-04-02T00:00:00", "2007-04-03T00:00:00"]
);
let updated = instance(&instances, "2007-04-03T00:00:00");
assert_eq!(updated.text_field("title"), "Spring break, day two");
assert_eq!(updated.text_field("duration"), "P1D");
account
.jmap_destroy(
MethodObject::CalendarEvent,
[&all_day_id],
Vec::<(&str, &str)>::new(),
)
.await
.destroyed()
.next()
.unwrap();
// Occurrences covered by a this-and-future change cannot be modified individually
let dav_client = account.webdav_client();
let account_id = account.id().document_id();
let resources = test
.server
.fetch_dav_resources(account_id, account_id, SyncCollection::Calendar)
.await
.unwrap();
let calendar_document_id = Id::from_str(&calendar_id).unwrap().document_id();
let calendar_path = resources
.paths
.iter()
.find(|path| {
path.parent_id.is_none()
&& resources.resources[path.resource_idx].document_id == calendar_document_id
})
.map(|path| format!("{}{}", resources.base_path, path.path))
.unwrap();
dav_client
.request(
"PUT",
&format!("{calendar_path}/this-and-future.ics"),
THIS_AND_FUTURE_ICAL,
)
.await
.with_status(StatusCode::CREATED);
test.wait_for_tasks().await;
let instances = expand_instances(account, &calendar_id).await;
assert_eq!(
starts(&instances),
[
"2007-03-05T12:00:00",
"2007-03-06T12:00:00",
"2007-03-07T14:00:00",
"2007-03-08T14:00:00",
"2007-03-09T14:00:00"
]
);
assert_eq!(
instance(&instances, "2007-03-07T14:00:00").text_field("recurrenceId"),
"2007-03-07T12:00:00"
);
for start in ["2007-03-08T14:00:00", "2007-03-09T14:00:00"] {
assert_eq!(
instance(&instances, start).text_field("recurrenceId"),
start
);
let id = instance_id(&instances, start);
let response = account
.jmap_update(
MethodObject::CalendarEvent,
[(&id, json!({"title": "Should not apply"}))],
Vec::<(&str, &str)>::new(),
)
.await;
assert_eq!(response.not_updated(&id).typ(), "invalidProperties");
assert_eq!(
response.not_updated(&id).description(),
"Occurrences of a this-and-future change cannot be modified individually."
);
let response = account
.jmap_destroy(
MethodObject::CalendarEvent,
[&id],
Vec::<(&str, &str)>::new(),
)
.await;
assert_eq!(response.not_destroyed(&id).typ(), "invalidProperties");
}
// The occurrence the this-and-future change starts on is still editable
let id = instance_id(&instances, "2007-03-07T14:00:00");
account
.jmap_update(
MethodObject::CalendarEvent,
[(&id, json!({"title": "Split renamed"}))],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&id);
test.wait_for_tasks().await;
let instances = expand_instances(account, &calendar_id).await;
assert_eq!(
starts(&instances),
[
"2007-03-05T12:00:00",
"2007-03-06T12:00:00",
"2007-03-07T14:00:00",
"2007-03-08T14:00:00",
"2007-03-09T14:00:00"
]
);
assert_eq!(
instance(&instances, "2007-03-07T14:00:00").text_field("title"),
"Split renamed"
);
dav_client
.request(
"DELETE",
&format!("{calendar_path}/this-and-future.ics"),
"",
)
.await
.with_status(StatusCode::NO_CONTENT);
// Patching an occurrence does not recompute its duration
dav_client
.request(
"PUT",
&format!("{calendar_path}/night-shift.ics"),
NIGHT_SHIFT_ICAL,
)
.await
.with_status(StatusCode::CREATED);
test.wait_for_tasks().await;
let instances = expand_instances(account, &calendar_id).await;
assert_eq!(
starts(&instances),
["2007-03-10T23:00:00", "2007-03-11T23:00:00"]
);
assert_eq!(
instance(&instances, "2007-03-10T23:00:00").text_field("duration"),
"PT5H"
);
let id = instance_id(&instances, "2007-03-10T23:00:00");
account
.jmap_update(
MethodObject::CalendarEvent,
[(&id, json!({"title": "Night shift, renamed"}))],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&id);
test.wait_for_tasks().await;
let updated = expand_instances(account, &calendar_id).await;
let updated = instance(&updated, "2007-03-10T23:00:00");
assert_eq!(updated.text_field("title"), "Night shift, renamed");
assert_eq!(updated.text_field("duration"), "PT5H");
dav_client
.request("DELETE", &format!("{calendar_path}/night-shift.ics"), "")
.await
.with_status(StatusCode::NO_CONTENT);
// Synthetic ids keep identifying the same occurrence across writes
let series_calendar_id = account
.jmap_create(
MethodObject::Calendar,
[json!({
"name": "Stable instance ids",
"timeZone": "US/Eastern",
})],
Vec::<(&str, &str)>::new(),
)
.await
.created(0)
.id()
.to_string();
account
.jmap_create(
MethodObject::CalendarEvent,
[json!({
"@type": "Event",
"uid": "[email protected]",
"title": "Weekly sync",
"start": "2007-04-02T09:00:00",
"duration": "PT1H",
"timeZone": "US/Eastern",
"updated": "2007-02-06T00:11:21Z",
"recurrenceRule": {
"frequency": "weekly",
"count": 5
},
"calendarIds": {
&series_calendar_id: true
}
})],
Vec::<(&str, &str)>::new(),
)
.await
.created(0);
test.wait_for_tasks().await;
let instances = expand_instances(account, &series_calendar_id).await;
let held_ids = instances
.iter()
.map(|instance| instance.id().to_string())
.collect::<Vec<_>>();
assert_eq!(
starts(&instances),
[
"2007-04-02T09:00:00",
"2007-04-09T09:00:00",
"2007-04-16T09:00:00",
"2007-04-23T09:00:00",
"2007-04-30T09:00:00"
]
);
let moved_id = instance_id(&instances, "2007-04-09T09:00:00");
account
.jmap_update(
MethodObject::CalendarEvent,
[(
&moved_id,
json!({
"title": "Weekly sync, moved",
"start": "2007-04-09T14:00:00"
}),
)],
Vec::<(&str, &str)>::new(),
)
.await
.updated(&moved_id);
test.wait_for_tasks().await;
let held = account
.jmap_get(
MethodObject::CalendarEvent,
["id", "start", "recurrenceId"],
held_ids.clone(),
)
.await
.list()
.to_vec();
let held_start = |id: &str| {
held.iter()
.find(|event| event.id() == id)
.unwrap_or_else(|| panic!("Missing instance {id}: {held:?}"))
.text_field("start")
};
assert_eq!(
held_ids.iter().map(|id| held_start(id)).collect::<Vec<_>>(),
[
"2007-04-02T09:00:00",
"2007-04-09T14:00:00",
"2007-04-16T09:00:00",
"2007-04-23T09:00:00",
"2007-04-30T09:00:00"
]
);
assert_eq!(
held.iter()
.find(|event| event.id() == held_ids[1])
.map(|event| event.text_field("recurrenceId")),
Some("2007-04-09T09:00:00")
);
// Destroying an id held across a write removes the occurrence it was issued for
assert_eq!(
account
.jmap_destroy(
MethodObject::CalendarEvent,
[&held_ids[2]],
Vec::<(&str, &str)>::new(),
)
.await
.destroyed()
.collect::<Vec<_>>(),
[held_ids[2].as_str()]
);
test.wait_for_tasks().await;
assert_eq!(
starts(&expand_instances(account, &series_calendar_id).await),
[
"2007-04-02T09:00:00",
"2007-04-09T14:00:00",
"2007-04-23T09:00:00",
"2007-04-30T09:00:00"
]
);
// Clean up
for calendar_id in [&calendar_id, &series_calendar_id] {
account
.jmap_destroy(
MethodObject::Calendar,
[calendar_id],
[("onDestroyRemoveEvents", true)],
)
.await;
}
}
async fn expand_instances(account: &Account, calendar_id: &str) -> Vec<Value> {
let ids = account
.jmap_query(
MethodObject::CalendarEvent,
[
("inCalendar", Value::String(calendar_id.to_string())),
("after", Value::String("2007-03-01T00:00:00".to_string())),
("before", Value::String("2007-05-01T00:00:00".to_string())),
],
["start"],
[
("timeZone", Value::String("US/Eastern".to_string())),
("expandRecurrences", Value::Bool(true)),
],
)
.await
.ids()
.map(|id| id.to_string())
.collect::<Vec<_>>();
if ids.is_empty() {
return Vec::new();
}
account
.jmap_get(
MethodObject::CalendarEvent,
[
"id",
"baseEventId",
"start",
"duration",
"title",
"recurrenceId",
"locations",
"recurrenceRule",
"recurrenceOverrides",
],
ids,
)
.await
.list()
.to_vec()
}
async fn base_event(account: &Account, id: &str) -> Value {
account
.jmap_get(
MethodObject::CalendarEvent,
["id", "title", "locations", "recurrenceOverrides"],
[id],
)
.await
.list()[0]
.clone()
}
fn starts(instances: &[Value]) -> Vec<&str> {
instances
.iter()
.map(|instance| instance.text_field("start"))
.collect()
}
fn instance<'x>(instances: &'x [Value], start: &str) -> &'x Value {
instances
.iter()
.find(|instance| instance.text_field("start") == start)
.unwrap_or_else(|| panic!("Missing instance starting at {start}: {instances:?}"))
}
fn instance_id(instances: &[Value], start: &str) -> String {
instance(instances, start).id().to_string()
}
const THIS_AND_FUTURE_ICAL: &str = concat!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Test//EN\r\n",
"BEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTAMP:20070206T001121Z\r\n",
"SUMMARY:Standup\r\nDTSTART;TZID=US/Eastern:20070305T120000\r\nDURATION:PT1H\r\n",
"RRULE:FREQ=DAILY;COUNT=5\r\nEND:VEVENT\r\n",
"BEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTAMP:20070206T001121Z\r\n",
"SUMMARY:Standup moved\r\n",
"RECURRENCE-ID;TZID=US/Eastern;RANGE=THISANDFUTURE:20070307T120000\r\n",
"DTSTART;TZID=US/Eastern:20070307T140000\r\nDURATION:PT1H\r\nEND:VEVENT\r\n",
"END:VCALENDAR\r\n"
);
const NIGHT_SHIFT_ICAL: &str = concat!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Test//EN\r\n",
"BEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTAMP:20070206T001121Z\r\n",
"SUMMARY:Night shift\r\nDTSTART;TZID=US/Eastern:20070310T230000\r\n",
"DTEND;TZID=US/Eastern:20070311T050000\r\nRRULE:FREQ=DAILY;COUNT=2\r\n",
"END:VEVENT\r\nEND:VCALENDAR\r\n"
);
+13
View File
@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod acl;
pub mod alarm;
pub mod calendars;
pub mod event;
pub mod identity;
pub mod instance;
pub mod notification;
+582
View File
@@ -0,0 +1,582 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::{
jmap::{IntoJmapSet, JmapUtils},
server::TestServer,
};
use calcard::jscalendar::JSCalendarProperty;
use jmap_proto::{
object::calendar_event_notification::CalendarEventNotificationProperty,
request::method::MethodObject,
};
use mail_parser::DateTime;
use serde_json::{Value, json};
use store::write::now;
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running Calendar Event Notification tests...");
let john = test.account("[email protected]");
let jane = test.account("[email protected]");
let bill = test.account("[email protected]");
let john_id = john.id_string().to_string();
let jane_id = jane.id_string().to_string();
let bill_id = bill.id_string().to_string();
let mut john_change_id = String::new();
let mut jane_change_id = String::new();
let mut bill_change_id = String::new();
// Obtain share notification change ids for all accounts
for (change_id, client) in [
(&mut john_change_id, john),
(&mut jane_change_id, jane),
(&mut bill_change_id, bill),
] {
let response = client
.jmap_get(
MethodObject::CalendarEventNotification,
[CalendarEventNotificationProperty::Id],
Vec::<&str>::new(),
)
.await;
response.list_array().assert_is_equal(json!([]));
*change_id = response.state().to_string();
let response = client
.jmap_changes(MethodObject::CalendarEventNotification, &change_id)
.await;
assert_eq!(response.changes().next(), None);
assert_eq!(response.new_state(), change_id.as_str());
}
// Create test calendars
let response = john
.jmap_create(
MethodObject::Calendar,
[json!({
"name": "Test Calendar",
})],
Vec::<(&str, &str)>::new(),
)
.await;
let john_calendar_id = response.created(0).id().to_string();
// Sent invitation to Jane and Bill
let john_event = test_event();
let response = john
.jmap_create(
MethodObject::CalendarEvent,
[john_event.clone().with_property(
JSCalendarProperty::<Id>::CalendarIds,
[john_calendar_id.as_str()].into_jmap_set(),
)],
[("sendSchedulingMessages", true)],
)
.await;
let john_event_id = response.created(0).id().to_string();
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
test.wait_for_tasks().await;
// Verify Jane and Bill received the share notification
let mut jane_event_id = String::new();
let mut bill_event_id = String::new();
for (change_id, event_id, client) in [
(&mut jane_change_id, &mut jane_event_id, jane),
(&mut bill_change_id, &mut bill_event_id, bill),
] {
// Obtain changes
let response = client
.jmap_changes(MethodObject::CalendarEventNotification, &change_id)
.await;
let changes = response.changes().collect::<Vec<_>>();
assert_eq!(changes.len(), 1);
*change_id = response.new_state().to_string();
let notification_id = changes[0].as_created();
// Obtain and verify notification
let response = client
.jmap_get(
MethodObject::CalendarEventNotification,
[
CalendarEventNotificationProperty::Id,
CalendarEventNotificationProperty::Created,
CalendarEventNotificationProperty::ChangedBy,
CalendarEventNotificationProperty::Comment,
CalendarEventNotificationProperty::Type,
CalendarEventNotificationProperty::CalendarEventId,
CalendarEventNotificationProperty::IsDraft,
CalendarEventNotificationProperty::Event,
CalendarEventNotificationProperty::EventPatch,
],
[notification_id],
)
.await;
let notification = &response.list()[0];
*event_id = notification.text_field("calendarEventId").to_string();
notification.assert_is_equal(json!({
"id": &notification_id,
"created": &notification.text_field("created"),
"changedBy": {
"name": "John Doe",
"email": "[email protected]",
"principalId": &john_id
},
"type": "created",
"calendarEventId": event_id,
"isDraft": false,
"event": john_event
.clone()
.with_property(
"updated",
notification
.text_field("event/updated")
)
}));
// Verify the event exists
let response = client
.jmap_get(
MethodObject::CalendarEvent,
[JSCalendarProperty::<Id>::Id, JSCalendarProperty::Title],
[&event_id],
)
.await;
response.list()[0].assert_is_equal(json!({
"id": &event_id,
"title": "Lunch"
}));
}
// Jane and Bill accept the invitation
let response = jane
.jmap_update(
MethodObject::CalendarEvent,
[(
&jane_event_id,
json!({
"participants/a0171748-fe8d-57d8-879e-56036a5251d1/participationStatus":
"accepted"}),
)],
[("sendSchedulingMessages", true)],
)
.await;
response.updated(&jane_event_id);
let response = bill
.jmap_update(
MethodObject::CalendarEvent,
[(
&bill_event_id,
json!({
"participants/86720268-d67c-58c3-9217-03df7d7ee4d8/participationStatus":
"accepted"}),
)],
[("sendSchedulingMessages", true)],
)
.await;
response.updated(&bill_event_id);
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
// Verify John received two share notifications
let response = john
.jmap_changes(MethodObject::CalendarEventNotification, &john_change_id)
.await;
let changes = response.changes().collect::<Vec<_>>();
assert_eq!(changes.len(), 2);
for (i, change) in changes.into_iter().enumerate() {
let notification_id = change.as_created();
// Obtain and verify notification
let response = john
.jmap_get(
MethodObject::CalendarEventNotification,
[
CalendarEventNotificationProperty::Id,
CalendarEventNotificationProperty::ChangedBy,
CalendarEventNotificationProperty::Comment,
CalendarEventNotificationProperty::Type,
CalendarEventNotificationProperty::CalendarEventId,
CalendarEventNotificationProperty::IsDraft,
],
[notification_id],
)
.await;
let changed_by = if i == 0 {
json!({
"name": "Jane Smith",
"email": "[email protected]",
"principalId": &jane_id,
})
} else {
json!({
"name": "Bill Foobar",
"email": "[email protected]",
"principalId": &bill_id,
})
};
response.list()[0].assert_is_equal(json!({
"id": &notification_id,
"changedBy": changed_by,
"type": "updated",
"calendarEventId": &john_event_id,
"isDraft": false
}));
}
// Verify the event was updated
let response = john
.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::Title,
JSCalendarProperty::Participants,
],
[&john_event_id],
)
.await;
response.list()[0].assert_is_equal(json!({
"participants": {
"8584f8f9-5414-55e3-8a1c-ad6fc2f3ffb6": {
"calendarAddress": "mailto:[email protected]",
"@type": "Participant",
"roles": {
"chair": true,
"owner": true
},
"participationStatus": "accepted"
},
"a0171748-fe8d-57d8-879e-56036a5251d1": {
"calendarAddress": "mailto:[email protected]",
"@type": "Participant",
"participationStatus": "accepted",
"kind": "individual"
},
"86720268-d67c-58c3-9217-03df7d7ee4d8": {
"calendarAddress": "mailto:[email protected]",
"@type": "Participant",
"kind": "individual",
"participationStatus": "accepted"
}
},
"title": "Lunch",
"id": &john_event_id
}));
// Jane later declines the invitation
let response = jane
.jmap_update(
MethodObject::CalendarEvent,
[(
&jane_event_id,
json!({
"participants/a0171748-fe8d-57d8-879e-56036a5251d1/participationStatus":
"declined"}),
)],
[("sendSchedulingMessages", true)],
)
.await;
response.updated(&jane_event_id);
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
// Make sure John received the update
let response = john
.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::Title,
JSCalendarProperty::Participants,
],
[&john_event_id],
)
.await;
response.list()[0].assert_is_equal(json!({
"participants": {
"8584f8f9-5414-55e3-8a1c-ad6fc2f3ffb6": {
"calendarAddress": "mailto:[email protected]",
"@type": "Participant",
"roles": {
"chair": true,
"owner": true
},
"participationStatus": "accepted"
},
"a0171748-fe8d-57d8-879e-56036a5251d1": {
"calendarAddress": "mailto:[email protected]",
"@type": "Participant",
"participationStatus": "declined",
"kind": "individual"
},
"86720268-d67c-58c3-9217-03df7d7ee4d8": {
"calendarAddress": "mailto:[email protected]",
"@type": "Participant",
"kind": "individual",
"participationStatus": "accepted"
}
},
"title": "Lunch",
"id": &john_event_id
}));
// John deletes the event
let response = john
.jmap_destroy(
MethodObject::CalendarEvent,
[&john_event_id],
[("sendSchedulingMessages", true)],
)
.await;
assert_eq!(response.destroyed().collect::<Vec<_>>(), [&john_event_id]);
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
// Verify that only Bill received the cancellation
let response = jane
.jmap_changes(MethodObject::CalendarEventNotification, &jane_change_id)
.await;
assert_eq!(response.changes().next(), None);
let response = bill
.jmap_changes(MethodObject::CalendarEventNotification, &bill_change_id)
.await;
let changes = response.changes().collect::<Vec<_>>();
assert_eq!(changes.len(), 1);
let notification_id = changes[0].as_created();
let response = bill
.jmap_get(
MethodObject::CalendarEventNotification,
[
CalendarEventNotificationProperty::Id,
CalendarEventNotificationProperty::ChangedBy,
CalendarEventNotificationProperty::Comment,
CalendarEventNotificationProperty::Type,
CalendarEventNotificationProperty::CalendarEventId,
CalendarEventNotificationProperty::IsDraft,
],
[notification_id],
)
.await;
response.list()[0].assert_is_equal(json!({
"id": &notification_id,
"changedBy": {
"name": "John Doe",
"email": "[email protected]",
"principalId": &john_id
},
"type": "updated",
"calendarEventId": &bill_event_id,
"isDraft": false
}));
// Verify Bill's event was updated
let response = bill
.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::Title,
JSCalendarProperty::Status,
],
[&bill_event_id],
)
.await;
response.list()[0].assert_is_equal(json!({
"id": &bill_event_id,
"title": "Lunch",
"status": "cancelled"
}));
// Scheduling messages are sent for a server-assigned organizer
let jane_change_id = jane
.jmap_get(
MethodObject::CalendarEventNotification,
[CalendarEventNotificationProperty::Id],
Vec::<&str>::new(),
)
.await
.state()
.to_string();
let response = john
.jmap_create(
MethodObject::CalendarEvent,
[test_event_without_organizer().with_property(
JSCalendarProperty::<Id>::CalendarIds,
[john_calendar_id.as_str()].into_jmap_set(),
)],
[("sendSchedulingMessages", true)],
)
.await;
let john_event_id = response.created(0).id().to_string();
john.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::OrganizerCalendarAddress,
],
[&john_event_id],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": &john_event_id,
"organizerCalendarAddress": "mailto:[email protected]"
}));
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
test.wait_for_tasks().await;
let response = jane
.jmap_changes(MethodObject::CalendarEventNotification, &jane_change_id)
.await;
let changes = response.changes().collect::<Vec<_>>();
assert_eq!(changes.len(), 1);
let notification_id = changes[0].as_created();
let jane_event_id = jane
.jmap_get(
MethodObject::CalendarEventNotification,
[
CalendarEventNotificationProperty::Id,
CalendarEventNotificationProperty::CalendarEventId,
],
[notification_id],
)
.await
.list()[0]
.text_field("calendarEventId")
.to_string();
jane.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::Title,
JSCalendarProperty::OrganizerCalendarAddress,
],
[&jane_event_id],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": &jane_event_id,
"title": "Brunch",
"organizerCalendarAddress": "mailto:[email protected]"
}));
// An attendee replying to the invitation does not take over as organizer
jane.jmap_update(
MethodObject::CalendarEvent,
[(
&jane_event_id,
json!({
"participants/a0171748-fe8d-57d8-879e-56036a5251d1/participationStatus":
"accepted"}),
)],
[("sendSchedulingMessages", true)],
)
.await
.updated(&jane_event_id);
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
jane.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::OrganizerCalendarAddress,
],
[&jane_event_id],
)
.await
.list()[0]
.assert_is_equal(json!({
"id": &jane_event_id,
"organizerCalendarAddress": "mailto:[email protected]"
}));
// Cleanup
test.wait_for_tasks().await;
for client in [john, jane, bill] {
client.destroy_all_calendars().await;
client.destroy_all_event_notifications().await;
test.destroy_all_mailboxes(client).await;
}
test.assert_is_empty().await;
}
fn test_event_without_organizer() -> Value {
json!({
"uid": "9263504FD3AE",
"title": "Brunch",
"timeZone": "Europe/London",
"start": DateTime::from_timestamp(now() as i64 + 60 * 60)
.to_rfc3339().trim_end_matches("Z").to_string(),
"duration": "PT1H",
"freeBusyStatus": "busy",
"updated": "2009-06-02T17:00:00Z",
"sequence": 0,
"@type": "Event",
"participants": {
"8584f8f9-5414-55e3-8a1c-ad6fc2f3ffb6": {
"calendarAddress": "mailto:[email protected]",
"participationStatus": "accepted",
"roles": {
"chair": true,
"owner": true
},
"@type": "Participant"
},
"a0171748-fe8d-57d8-879e-56036a5251d1": {
"calendarAddress": "mailto:[email protected]",
"@type": "Participant",
"participationStatus": "needs-action",
"kind": "individual"
}
}
})
}
fn test_event() -> Value {
json!({
"uid": "9263504FD3AD",
"title": "Lunch",
"timeZone": "Europe/London",
"start": DateTime::from_timestamp(now() as i64 + 60 * 60)
.to_rfc3339().trim_end_matches("Z").to_string(),
"duration": "PT1H",
"freeBusyStatus": "busy",
"updated": "2009-06-02T17:00:00Z",
"sequence": 0,
"@type": "Event",
"participants": {
"8584f8f9-5414-55e3-8a1c-ad6fc2f3ffb6": {
"calendarAddress": "mailto:[email protected]",
"participationStatus": "accepted",
"roles": {
"chair": true,
"owner": true
},
"@type": "Participant"
},
"a0171748-fe8d-57d8-879e-56036a5251d1": {
"calendarAddress": "mailto:[email protected]",
"@type": "Participant",
"participationStatus": "needs-action",
"kind": "individual"
},
"86720268-d67c-58c3-9217-03df7d7ee4d8": {
"calendarAddress": "mailto:[email protected]",
"participationStatus": "needs-action",
"@type": "Participant",
"kind": "individual"
}
},
"organizerCalendarAddress": "mailto:[email protected]"
})
}