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:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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": ¬ification_id,
|
||||
"created": ¬ification.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": ¬ification_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": ¬ification_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]"
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user