JMAP for Calendat tests (Calendars and Events)

This commit is contained in:
mdecimus
2025-10-15 19:27:32 +02:00
parent 62eb087c37
commit 96d2947cdd
30 changed files with 1630 additions and 310 deletions

View File

@@ -4,11 +4,369 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::jmap::{JMAPTest, JmapUtils};
use jmap_proto::request::method::MethodObject;
use crate::jmap::{ChangeType, JMAPTest, JmapUtils};
use jmap_proto::{object::calendar::CalendarProperty, request::method::MethodObject};
use serde_json::json;
pub async fn test(params: &mut JMAPTest) {
println!("Running tests...");
println!("Running Calendar tests...");
let account = params.account("jdoe@example.com");
// Make sure the default calendar exists
let response = account
.jmap_get(
MethodObject::Calendar,
[
CalendarProperty::Id,
CalendarProperty::Name,
CalendarProperty::Description,
CalendarProperty::SortOrder,
CalendarProperty::Color,
CalendarProperty::TimeZone,
CalendarProperty::IsSubscribed,
CalendarProperty::IsDefault,
CalendarProperty::IsVisible,
CalendarProperty::IncludeInAvailability,
CalendarProperty::DefaultAlertsWithTime,
CalendarProperty::DefaultAlertsWithoutTime,
],
Vec::<&str>::new(),
)
.await;
let list = response.list();
assert_eq!(list.len(), 1);
let default_calendar_id = list[0].id().to_string();
assert_eq!(
list[0],
json!({
"id": default_calendar_id,
"name": "Stalwart Calendar (jdoe@example.com)",
"description": null,
"sortOrder": 0,
"isSubscribed": false,
"isDefault": true,
"color": null,
"timeZone": null,
"isVisible": true,
"includeInAvailability": "all",
"defaultAlertsWithTime": {},
"defaultAlertsWithoutTime": {}
})
);
let change_id = response.state();
// Create Calendar
let calendar_id = account
.jmap_create(
MethodObject::Calendar,
[json!({
"name": "Test calendar",
"description": "My personal calendar",
"sortOrder": 1,
"isSubscribed": true,
"color": "#ff0000",
"timeZone": "Indian/Christmas",
"isVisible": false,
"includeInAvailability": "attending",
"defaultAlertsWithTime": {
"0": {
"action": "display",
"trigger": {
"relativeTo": "start",
"offset": "PT15M"
}
},
"1": {
"action": "email",
"trigger": {
"relativeTo": "end",
"offset": "PT30M"
}
}
},
"defaultAlertsWithoutTime": {
"0": {
"action": "display",
"trigger": {
"relativeTo": "start",
"offset": "P1D"
}
},
"1": {
"action": "email",
"trigger": {
"relativeTo": "end",
"offset": "P2D"
}
}
}
})],
)
.await
.created(0)
.id()
.to_string();
// Validate changes
assert_eq!(
account
.jmap_changes(MethodObject::Calendar, change_id)
.await
.changes()
.collect::<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 (jdoe@example.com)",
"description": (),
"sortOrder": 0,
"isSubscribed": false,
"isDefault": false,
"color": null,
"timeZone": null,
"isVisible": true,
"includeInAvailability": "all",
"defaultAlertsWithTime": {},
"defaultAlertsWithoutTime": {}
}));
// Create an event
let _ = account
.jmap_create(
MethodObject::CalendarEvent,
[json!({
"calendarIds": {
&calendar_id: true
},
"@type": "Event",
"uid": "a8df6573-0474-496d-8496-033ad45d7fea",
"updated": "2020-01-02T18:23:04Z",
"title": "Some event",
"start": "2020-01-15T13:00:00",
"timeZone": "America/New_York",
"duration": "PT1H"
})],
)
.await
.created(0)
.id();
// Try destroying the calendar (should fail)
assert_eq!(
account
.jmap_destroy(
MethodObject::Calendar,
[&calendar_id],
Vec::<(&str, &str)>::new(),
)
.await
.not_destroyed(&calendar_id)
.typ(),
"calendarHasEvent"
);
// Destroy using force
assert_eq!(
account
.jmap_destroy(
MethodObject::Calendar,
[&calendar_id],
[("onDestroyRemoveEvents", true)],
)
.await
.destroyed()
.collect::<Vec<_>>(),
vec![&calendar_id]
);
// Destroy all mailboxes
account.destroy_all_calendars().await;
params.assert_is_empty().await;
}

View File

@@ -4,13 +4,780 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::jmap::{JMAPTest, JmapUtils};
use crate::{
jmap::{ChangeType, IntoJmapSet, JMAPTest, JmapUtils},
webdav::DummyWebDavClient,
};
use ahash::AHashSet;
use calcard::jscalendar::JSCalendarProperty;
use groupware::cache::GroupwareCache;
use hyper::StatusCode;
use jmap_proto::request::method::MethodObject;
use serde_json::json;
use serde_json::{Value, json};
use types::{collection::SyncCollection, id::Id};
pub async fn test(params: &mut JMAPTest) {
println!("Running tests...");
let account = params.account("jdoe@example.com");
// Create test calendars
let response = account
.jmap_create(
MethodObject::Calendar,
[
json!({
"name": "Holy Calendar, Batman!",
"timeZone": "Europe/Vatican",
}),
json!({
"name": "Calendar with Alerts",
"defaultAlertsWithTime": {
"abc": {
"action": "display",
"trigger": {
"relativeTo": "start",
"offset": "PT15M"
}
}
},
}),
],
)
.await;
let calendar1_id = response.created(0).id().to_string();
let calendar2_id = response.created(1).id().to_string();
// Obtain state
let change_id = account
.jmap_get(
MethodObject::CalendarEvent,
Vec::<&str>::new(),
Vec::<&str>::new(),
)
.await
.state()
.to_string();
// Create test events
let event_1 = test_jscalendar_1().with_property(
JSCalendarProperty::<Id>::CalendarIds,
[calendar1_id.as_str()].into_jmap_set(),
);
let event_2 = test_jscalendar_2().with_property(
JSCalendarProperty::<Id>::CalendarIds,
[calendar2_id.as_str()].into_jmap_set(),
);
let event_3 = test_jscalendar_3().with_property(
JSCalendarProperty::<Id>::CalendarIds,
[calendar1_id.as_str(), calendar2_id.as_str()].into_jmap_set(),
);
let response = account
.jmap_create(
MethodObject::CalendarEvent,
[
event_1
.clone()
.with_property(JSCalendarProperty::<Id>::IsDraft, true)
.with_property(JSCalendarProperty::<Id>::MayInviteSelf, true)
.with_property(JSCalendarProperty::<Id>::MayInviteOthers, true)
.with_property(JSCalendarProperty::<Id>::HideAttendees, true),
event_2
.clone()
.with_property(JSCalendarProperty::<Id>::UseDefaultAlerts, true),
event_3.clone(),
],
)
.await;
let event_1_id = response.created(0).id().to_string();
let event_2_id = response.created(1).id().to_string();
let event_3_id = response.created(2).id().to_string();
// Validate changes
assert_eq!(
account
.jmap_changes(MethodObject::CalendarEvent, &change_id)
.await
.changes()
.collect::<AHashSet<_>>(),
[
ChangeType::Created(&event_1_id),
ChangeType::Created(&event_2_id),
ChangeType::Created(&event_3_id)
]
.into_iter()
.collect::<AHashSet<_>>(),
);
// Verify event contents
let response = account
.jmap_get(
MethodObject::CalendarEvent,
Vec::<&str>::new(),
[&event_1_id, &event_2_id, &event_3_id],
)
.await;
response.list()[0].assert_is_equal(
event_1
.with_property(JSCalendarProperty::<Id>::Id, event_1_id.as_str())
.with_property(JSCalendarProperty::<Id>::IsDraft, true)
.with_property(JSCalendarProperty::<Id>::IsOrigin, true),
);
response.list()[1].assert_is_equal(
event_2
.with_property(JSCalendarProperty::<Id>::Id, event_2_id.as_str())
.with_property(JSCalendarProperty::<Id>::IsDraft, false)
.with_property(JSCalendarProperty::<Id>::IsOrigin, true)
.with_property(
JSCalendarProperty::<Id>::Alerts,
json!({
"k1": {
"action": "display",
"trigger": {
"@type": "OffsetTrigger",
"offset": "PT15M"
},
"@type": "Alert"
}
}),
),
);
response.list()[2].assert_is_equal(
event_3
.with_property(JSCalendarProperty::<Id>::Id, event_3_id.as_str())
.with_property(JSCalendarProperty::<Id>::IsDraft, false)
.with_property(JSCalendarProperty::<Id>::IsOrigin, false),
);
// Verify JMAP for Calendars properties
let response = account
.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::MayInviteSelf,
JSCalendarProperty::MayInviteOthers,
JSCalendarProperty::HideAttendees,
JSCalendarProperty::UtcStart,
JSCalendarProperty::UtcEnd,
],
[&event_1_id, &event_2_id, &event_3_id],
)
.await;
response.list()[0].assert_is_equal(json!({
"id": &event_1_id,
"mayInviteSelf": true,
"mayInviteOthers": true,
"hideAttendees": true,
"utcStart": "2006-01-02T15:00:00Z",
"utcEnd": "2006-01-02T16:00:00Z"
}));
response.list()[1].assert_is_equal(json!({
"id": &event_2_id,
"mayInviteSelf": false,
"mayInviteOthers": false,
"hideAttendees": false,
"utcStart": "2006-01-02T17:00:00Z",
"utcEnd": "2006-01-02T18:00:00Z"
}));
response.list()[2].assert_is_equal(json!({
"id": &event_3_id,
"mayInviteSelf": false,
"mayInviteOthers": false,
"hideAttendees": false,
"utcStart": "2006-01-04T15:00:00Z",
"utcEnd": "2006-01-04T16:00:00Z"
}));
// Test /get parameters
let response = account
.jmap_method_calls(json!([[
"CalendarEvent/get",
{
"properties": ["id", "title", "recurrenceOverrides", "participants"],
"ids": [&event_2_id, &event_3_id],
"recurrenceOverridesBefore": "2006-01-07T00:00:00Z",
"recurrenceOverridesAfter": "2006-01-06T00:00:00Z",
"reduceParticipants": true,
},
"0"
]]))
.await;
response.list_array().assert_is_equal(json!([
{
"title": "Event #2",
"recurrenceOverrides": {
"2006-01-06T12:00:00": {
"updated": "2006-02-06T00:11:21Z",
"start": "2006-01-06T14:00:00",
"title": "Event #2 bis bis",
"duration": "PT1H"
}
},
"id": "c"
},
{
"title": "Event #3",
"participants": {
"3f5bc8c0-c722-5345-b7d9-5a899db08a30": {
"calendarAddress": "mailto:cyrus@example.com",
"@type": "Participant"
}
},
"id": "d"
}
]));
// Creating an event without calendar should fail
assert_eq!(
account
.jmap_create(
MethodObject::CalendarEvent,
[json!({
"title": "Event #5",
"start": "2006-01-22T10:00:00",
"duration": "PT1H",
"timeZone": "US/Eastern",
"calendarIds": {},
}),],
)
.await
.not_created(0)
.description(),
"Event has to belong to at least one calendar."
);
// Creating an event with a duplicate UID should fail
assert_eq!(
account
.jmap_create(
MethodObject::CalendarEvent,
[json!({
"title": "Event #5",
"start": "2006-01-22T10:00:00",
"duration": "PT1H",
"timeZone": "US/Eastern",
"uid": "00959BC664CA650E933C892C@example.com",
"calendarIds": {
&calendar1_id: true
},
})],
)
.await
.not_created(0)
.description(),
"An event with UID 00959BC664CA650E933C892C@example.com already exists."
);
// Patching tests
let response = account
.jmap_update(
MethodObject::CalendarEvent,
[
(
&event_1_id,
json!({
"isDraft": false,
"mayInviteSelf": false,
"mayInviteOthers": false,
"hideAttendees": false,
"description": null,
"title": "Event one",
"keywords": {"work": true},
format!("calendarIds/{calendar2_id}"): true
}),
),
(
&event_2_id,
json!({
"calendarIds": {
&calendar1_id: true,
&calendar2_id: true
},
"title": "Event two",
"description": "Updated description",
"recurrenceOverrides/2006-01-04T12:00:00/title":
"Event two overridden",
"recurrenceOverrides/2006-01-06T12:00:00/title":
"Event two overridden twice",
}),
),
(
&event_3_id,
json!({
format!("calendarIds/{calendar2_id}"): false,
"title": "Event three",
"utcStart": "2006-01-04T14:00:00Z",
"utcEnd": "2006-01-04T16:00:00Z",
"participants/3f5bc8c0-c722-5345-b7d9-5a899db08a30/roles/chair": false,
"participants/3f5bc8c0-c722-5345-b7d9-5a899db08a30/roles/owner": true,
"participants/ec5e7db5-22a3-5ed5-89bf-c8894ab86805" : null,
"participants/7f2bd210-6c66-5b64-8562-0176b74462b1": {
"calendarAddress": "mailto:rupert@example.com",
"@type": "Participant",
"roles": {
"attendee": true
},
"participationStatus": "needs-action"
}
}),
),
],
Vec::<(&str, &str)>::new(),
)
.await;
response.updated(&event_1_id);
response.updated(&event_2_id);
response.updated(&event_3_id);
// Verify patches
let response = account
.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::CalendarIds,
JSCalendarProperty::Title,
JSCalendarProperty::Start,
JSCalendarProperty::Description,
JSCalendarProperty::Keywords,
JSCalendarProperty::RecurrenceOverrides,
JSCalendarProperty::Participants,
JSCalendarProperty::MayInviteOthers,
JSCalendarProperty::MayInviteSelf,
JSCalendarProperty::HideAttendees,
JSCalendarProperty::IsDraft,
],
[&event_1_id, &event_2_id, &event_3_id],
)
.await;
response.list()[0].assert_is_equal(json!({
"id": &event_1_id,
"calendarIds": {
&calendar1_id: true,
&calendar2_id: true
},
"isDraft": false,
"mayInviteSelf": false,
"mayInviteOthers": false,
"hideAttendees": false,
"title": "Event one",
"start": "2006-01-02T10:00:00",
"keywords": {
"work": true
}
}));
response.list()[1].assert_is_equal(json!({
"id": &event_2_id,
"calendarIds": {
&calendar1_id: true,
&calendar2_id: true
},
"title": "Event two",
"start": "2006-01-02T12:00:00",
"description": "Updated description",
"recurrenceOverrides": {
"2006-01-04T12:00:00": {
"title": "Event two overridden",
"start": "2006-01-04T14:00:00",
"duration": "PT1H",
"updated": "2006-02-06T00:11:21Z"
},
"2006-01-06T12:00:00": {
"title": "Event two overridden twice",
"start": "2006-01-06T14:00:00",
"duration": "PT1H",
"updated": "2006-02-06T00:11:21Z"
}
},
"title": "Event two",
"start": "2006-01-02T12:00:00",
"mayInviteOthers": false,
"mayInviteSelf": false,
"hideAttendees": false,
"isDraft": false
}));
response.list()[2].assert_is_equal(json!({
"id": event_3_id,
"calendarIds": {
&calendar1_id: true,
},
"title": "Event three",
"start": "2006-01-04T09:00:00",
"participants": {
"3f5bc8c0-c722-5345-b7d9-5a899db08a30": {
"calendarAddress": "mailto:cyrus@example.com",
"@type": "Participant",
"roles": {
"attendee": true,
"owner": true
},
"participationStatus": "accepted"
},
"7f2bd210-6c66-5b64-8562-0176b74462b1": {
"calendarAddress": "mailto:rupert@example.com",
"@type": "Participant",
"roles": {
"attendee": true
},
"participationStatus": "needs-action"
}
},
"mayInviteOthers": false,
"mayInviteSelf": false,
"hideAttendees": false,
"isDraft": false
}));
// Query tests
assert_eq!(
account
.jmap_query(
MethodObject::CalendarEvent,
[
("text", "Event one"),
("inCalendar", calendar1_id.as_str()),
("uid", "74855313FA803DA593CD579A@example.com"),
("after", "2006-01-02T10:59:59"),
("before", "2006-01-02T10:00:01"),
],
["start"],
[("timeZone", "US/Eastern")],
)
.await
.ids()
.collect::<AHashSet<_>>(),
[event_1_id.as_str()].into_iter().collect::<AHashSet<_>>()
);
// Recurrence expansion tests
let response = account
.jmap_query(
MethodObject::CalendarEvent,
[
("after", "2006-01-01T00:00:00"),
("before", "2006-01-08T00:00:00"),
],
["start"],
[
("timeZone", Value::String("US/Eastern".into())),
("expandRecurrences", Value::Bool(true)),
],
)
.await;
let ids = response.ids().collect::<Vec<_>>();
assert_eq!(ids.len(), 7);
account
.jmap_get(
MethodObject::CalendarEvent,
[
JSCalendarProperty::<Id>::Id,
JSCalendarProperty::BaseEventId,
JSCalendarProperty::Start,
JSCalendarProperty::Duration,
JSCalendarProperty::Title,
JSCalendarProperty::RecurrenceId,
],
ids.clone(),
)
.await
.list_array()
.assert_is_equal(json!([
{
"duration": "PT1H",
"title": "Event one",
"start": "2006-01-02T15:00:00",
"id": &ids[0],
"baseEventId": &event_1_id
},
{
"recurrenceId": "2006-01-02T17:00:00",
"title": "Event two",
"duration": "PT1H",
"start": "2006-01-02T17:00:00",
"id": &ids[1],
"baseEventId": &event_2_id
},
{
"duration": "PT1H",
"start": "2006-01-03T17:00:00",
"title": "Event two",
"recurrenceId": "2006-01-03T17:00:00",
"id": &ids[2],
"baseEventId": &event_2_id
},
{
"start": "2006-01-04T14:00:00",
"duration": "PT2H",
"title": "Event three",
"id": &ids[3],
"baseEventId": &event_3_id
},
{
"recurrenceId": "2006-01-04T19:00:00",
"title": "Event two overridden",
"start": "2006-01-04T19:00:00",
"duration": "PT1H",
"id": &ids[4],
"baseEventId": &event_2_id
},
{
"recurrenceId": "2006-01-05T17:00:00",
"duration": "PT1H",
"start": "2006-01-05T17:00:00",
"title": "Event two",
"id": &ids[5],
"baseEventId": &event_2_id
},
{
"recurrenceId": "2006-01-06T19:00:00",
"duration": "PT1H",
"title": "Event two overridden twice",
"start": "2006-01-06T19:00:00",
"id": &ids[6],
"baseEventId": &event_2_id
}
]));
// Parse tests
account
.jmap_method_calls(json!([
[
"Blob/upload",
{
"create": {
"ical": {
"data": [
{
"data:asText": r#"BEGIN:VCALENDAR
PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN
VERSION:2.0
BEGIN:VEVENT
DTSTAMP:19960704T120000Z
UID:uid1@example.com
ORGANIZER:mailto:jsmith@example.com
DTSTART:19960918T143000Z
DTEND:19960920T220000Z
STATUS:CONFIRMED
CATEGORIES:CONFERENCE
SUMMARY:Networld+Interop Conference
DESCRIPTION:Networld+Interop Conference
and Exhibit\nAtlanta World Congress Center\n
Atlanta\, Georgia
END:VEVENT
END:VCALENDAR
"#
}
]
}
}
},
"S4"
],
[
"CalendarEvent/parse",
{
"blobIds": [
"#ical"
]
},
"G4"
]
]))
.await
.pointer("/methodResponses/1/1/parsed")
.unwrap()
.as_object()
.unwrap()
.iter()
.next()
.unwrap()
.1
.assert_is_equal(json!([
{
"updated": "1996-07-04T12:00:00Z",
"title": "Networld+Interop Conference",
"description": "Networld+Interop Conferenceand Exhibit\nAtlanta World Congress Center\n",
"timeZone": "Etc/UTC",
"start": "1996-09-18T14:30:00",
"status": "confirmed",
"iCalComponent": {
"convertedProperties": {
"duration": {
"name": "DTEND"
}
},
"name": "vevent"
},
"@type": "Event",
"uid": "uid1@example.com",
"participants": {
"25d7647e-52fc-559b-88df-d66f08da079c": {
"calendarAddress": "mailto:jsmith@example.com",
"@type": "Participant"
}
},
"keywords": {
"CONFERENCE": true
},
"organizerCalendarAddress": "mailto:jsmith@example.com",
"duration": "P2DT7H30M"
}
]));
// Deletion tests
assert_eq!(
account
.jmap_destroy(
MethodObject::CalendarEvent,
[event_2_id.as_str(), event_3_id.as_str()],
Vec::<(&str, &str)>::new()
)
.await
.destroyed()
.collect::<AHashSet<_>>(),
[event_2_id.as_str(), event_3_id.as_str()]
.into_iter()
.collect::<AHashSet<_>>()
);
// CardDAV compatibility tests
let account_id = account.id().document_id();
let dav_client = DummyWebDavClient::new(
u32::MAX,
account.name(),
account.secret(),
account.emails()[0],
);
let resources = params
.server
.fetch_dav_resources(
&params.server.get_access_token(account_id).await.unwrap(),
account_id,
SyncCollection::Calendar,
)
.await
.unwrap();
let path = format!(
"{}{}",
resources.base_path,
resources
.paths
.iter()
.find(|v| v.parent_id.is_some())
.unwrap()
.path
);
let ical = dav_client
.request("GET", &path, "")
.await
.with_status(StatusCode::OK)
.expect_body()
.lines()
.map(String::from)
.collect::<AHashSet<_>>();
let expected_ical = TEST_ICAL_1
.lines()
.map(String::from)
.collect::<AHashSet<_>>();
assert_eq!(ical, expected_ical);
// Clean up
account.destroy_all_calendars().await;
params.assert_is_empty().await;
}
pub fn test_jscalendar_1() -> Value {
json!({
"duration": "PT1H",
"@type": "Event",
"description": "Go Steelers!",
"updated": "2006-02-06T00:11:02Z",
"timeZone": "US/Eastern",
"start": "2006-01-02T10:00:00",
"title": "Event #1",
"uid": "74855313FA803DA593CD579A@example.com"
})
}
pub fn test_jscalendar_2() -> Value {
json!({
"title": "Event #2",
"duration": "PT1H",
"updated": "2006-02-06T00:11:21Z",
"recurrenceRule": {
"frequency": "daily",
"count": 5
},
"start": "2006-01-02T12:00:00",
"uid": "00959BC664CA650E933C892C@example.com",
"@type": "Event",
"timeZone": "US/Eastern",
"recurrenceOverrides": {
"2006-01-04T12:00:00": {
"title": "Event #2 bis",
"start": "2006-01-04T14:00:00",
"updated": "2006-02-06T00:11:21Z",
"duration": "PT1H"
},
"2006-01-06T12:00:00": {
"title": "Event #2 bis bis",
"start": "2006-01-06T14:00:00",
"updated": "2006-02-06T00:11:21Z",
"duration": "PT1H"
}
}
})
}
pub fn test_jscalendar_3() -> Value {
json!({
"duration": "PT1H",
"organizerCalendarAddress": "mailto:cyrus@example.com",
"@type": "Event",
"start": "2006-01-04T10:00:00",
"status": "tentative",
"uid": "DC6C50A017428C5216A2F1CD@example.com",
"sequence": 1,
"participants": {
"3f5bc8c0-c722-5345-b7d9-5a899db08a30": {
"calendarAddress": "mailto:cyrus@example.com",
"@type": "Participant",
"roles": {
"attendee": true,
"chair": true
},
"participationStatus": "accepted"
},
"ec5e7db5-22a3-5ed5-89bf-c8894ab86805": {
"calendarAddress": "mailto:lisa@example.com",
"@type": "Participant",
"roles": {
"attendee": true
},
"participationStatus": "needs-action"
}
},
"title": "Event #3",
"updated": "2006-02-06T00:12:20Z",
"timeZone": "US/Eastern"
})
}
const TEST_ICAL_1: &str = r#"BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060102T100000
UID:74855313FA803DA593CD579A@example.com
DURATION:PT1H
SUMMARY:Event one
DTSTAMP:20060206T001102Z
CATEGORIES:work
END:VEVENT
END:VCALENDAR
"#;

View File

@@ -5,9 +5,7 @@
*/
pub mod acl;
pub mod availability;
pub mod calendars;
pub mod event;
pub mod identity;
pub mod notification;
pub mod principal;

View File

@@ -49,13 +49,25 @@ pub async fn test(params: &mut JMAPTest) {
.to_string();
// Create test contacts
let sarah_contact = test_jscontact_1().with_property(
JSContactProperty::<Id>::AddressBookIds,
[book1_id.as_str()].into_jmap_set(),
);
let carlos_contact = test_jscontact_2().with_property(
JSContactProperty::<Id>::AddressBookIds,
[book2_id.as_str()].into_jmap_set(),
);
let acme_contact = test_jscontact_3().with_property(
JSContactProperty::<Id>::AddressBookIds,
[book1_id.as_str(), book2_id.as_str()].into_jmap_set(),
);
let response = account
.jmap_create(
MethodObject::ContactCard,
[
test_jscontact_1([book1_id.as_str()]),
test_jscontact_2([book2_id.as_str()]),
test_jscontact_3([book1_id.as_str(), book2_id.as_str()]),
sarah_contact.clone(),
carlos_contact.clone(),
acme_contact.clone(),
],
)
.await;
@@ -83,97 +95,18 @@ pub async fn test(params: &mut JMAPTest) {
let response = account
.jmap_get(
MethodObject::ContactCard,
[
JSContactProperty::<Id>::Id,
JSContactProperty::AddressBookIds,
JSContactProperty::Name,
],
Vec::<&str>::new(),
[&sarah_contact_id, &carlos_contact_id, &acme_contact_id],
)
.await;
assert_eq!(
response.list()[0],
json!({
"id": &sarah_contact_id,
"name": {
"full": "Sarah Johnson",
"components": [
{
"kind": "surname",
"value": "Johnson"
},
{
"kind": "given",
"value": "Sarah"
},
{
"kind": "given2",
"value": "Marie"
},
{
"kind": "title",
"value": "Dr."
},
{
"kind": "credential",
"value": "Ph.D."
}
],
"isOrdered": true
},
"addressBookIds": {
&book1_id: true
},
})
response.list()[0].assert_is_equal(
sarah_contact.with_property(JSContactProperty::<Id>::Id, sarah_contact_id.as_str()),
);
assert_eq!(
response.list()[1],
json!({
"id": &carlos_contact_id,
"name": {
"components": [
{
"kind": "surname",
"value": "Rodriguez-Martinez"
},
{
"kind": "given",
"value": "Carlos"
},
{
"kind": "given2",
"value": "Alberto"
},
{
"kind": "title",
"value": "Mr."
},
{
"kind": "credential",
"value": "Jr."
}
],
"isOrdered": true,
"full": "Carlos Rodriguez-Martinez"
},
"addressBookIds": {
&book2_id: true
},
})
response.list()[1].assert_is_equal(
carlos_contact.with_property(JSContactProperty::<Id>::Id, carlos_contact_id.as_str()),
);
assert_eq!(
response.list()[2],
json!({
"id": acme_contact_id,
"addressBookIds": {
&book1_id: true,
&book2_id: true
},
"name": {
"full": "Acme Business Solutions Ltd."
},
})
response.list()[2].assert_is_equal(
acme_contact.with_property(JSContactProperty::<Id>::Id, acme_contact_id.as_str()),
);
// Creating a contact without address book should fail
@@ -390,6 +323,7 @@ pub async fn test(params: &mut JMAPTest) {
("email", "sarah.johnson@example.com"),
],
["created"],
Vec::<(&str, &str)>::new(),
)
.await
.ids()
@@ -535,11 +469,10 @@ END:VCARD"#
params.assert_is_empty().await;
}
fn test_jscontact_1(ids: impl IntoJmapSet) -> Value {
fn test_jscontact_1() -> Value {
json!({
"uid": "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"@type": "Card",
"addressBookIds": ids.into_jmap_set(),
"preferredLanguages": {
"k1": {
"language": "en",
@@ -579,7 +512,8 @@ fn test_jscontact_1(ids: impl IntoJmapSet) -> Value {
"kind": "credential",
"value": "Ph.D."
}
]
],
"isOrdered": true
},
"cryptoKeys": {
"k1": {
@@ -713,7 +647,8 @@ fn test_jscontact_1(ids: impl IntoJmapSet) -> Value {
}
],
"timeZone": "Etc/GMT+5",
"coordinates": "40.7128;-74.0060"
"coordinates": "40.7128;-74.0060",
"isOrdered": true
},
"k2": {
"contexts": {
@@ -742,7 +677,8 @@ fn test_jscontact_1(ids: impl IntoJmapSet) -> Value {
"kind": "country",
"value": "USA"
}
]
],
"isOrdered": true
}
},
"titles": {
@@ -770,9 +706,8 @@ fn test_jscontact_1(ids: impl IntoJmapSet) -> Value {
})
}
fn test_jscontact_2(ids: impl IntoJmapSet) -> Value {
fn test_jscontact_2() -> Value {
json!({
"addressBookIds": ids.into_jmap_set(),
"phones": {
"k1": {
"number": "+34-611-234-567",
@@ -861,7 +796,8 @@ fn test_jscontact_2(ids: impl IntoJmapSet) -> Value {
"value": "Jr."
}
],
"full": "Carlos Rodriguez-Martinez"
"full": "Carlos Rodriguez-Martinez",
"isOrdered": true
},
"nicknames": {
"k1": {
@@ -991,7 +927,8 @@ fn test_jscontact_2(ids: impl IntoJmapSet) -> Value {
}
],
"timeZone": "Etc/GMT-1",
"coordinates": "40.4168;-3.7038"
"coordinates": "40.4168;-3.7038",
"isOrdered": true
},
"k2": {
"contexts": {
@@ -1016,7 +953,8 @@ fn test_jscontact_2(ids: impl IntoJmapSet) -> Value {
"kind": "country",
"value": "Spain"
}
]
],
"isOrdered": true
}
},
"organizations": {
@@ -1032,9 +970,8 @@ fn test_jscontact_2(ids: impl IntoJmapSet) -> Value {
})
}
fn test_jscontact_3(ids: impl IntoJmapSet) -> Value {
fn test_jscontact_3() -> Value {
json!({
"addressBookIds": ids.into_jmap_set(),
"kind": "org",
"organizations": {
"k1": {
@@ -1098,8 +1035,7 @@ fn test_jscontact_3(ids: impl IntoJmapSet) -> Value {
}
},
"name": {
"full": "Acme Business Solutions Ltd.",
"components": []
"full": "Acme Business Solutions Ltd."
},
"notes": {
"k1": {
@@ -1180,7 +1116,8 @@ fn test_jscontact_3(ids: impl IntoJmapSet) -> Value {
}
],
"timeZone": "Etc/UTC",
"coordinates": "51.5074;-0.1278"
"coordinates": "51.5074;-0.1278",
"isOrdered": true
},
"k2": {
"contexts": {
@@ -1204,7 +1141,8 @@ fn test_jscontact_3(ids: impl IntoJmapSet) -> Value {
"kind": "country",
"value": "United Kingdom"
}
]
],
"isOrdered": true
}
},
"updated": "2023-04-15T15:30:00Z",

View File

@@ -65,6 +65,7 @@ pub mod contacts;
pub mod core;
pub mod files;
pub mod mail;
pub mod principal;
pub mod server;
#[tokio::test(flavor = "multi_thread")]
@@ -106,12 +107,15 @@ async fn jmap_tests() {
server::purge::test(&mut params).await;
server::enterprise::test(&mut params).await;*/
//contacts::addressbook::test(&mut params).await;
//contacts::contact::test(&mut params).await;
/*contacts::addressbook::test(&mut params).await;
contacts::contact::test(&mut params).await;
contacts::acl::test(&mut params).await;
//files::node::test(&mut params).await;
files::acl::test(&mut params).await;
files::node::test(&mut params).await;
files::acl::test(&mut params).await;*/
//calendar::calendars::test(&mut params).await;
calendar::event::test(&mut params).await;
if delete {
params.temp_dir.delete();
@@ -564,6 +568,7 @@ impl Account {
object: impl Display,
filter: impl IntoIterator<Item = (impl Display, impl Into<Value>)>,
sort_by: impl IntoIterator<Item = impl Display>,
arguments: impl IntoIterator<Item = (impl Display, impl Into<Value>)>,
) -> JmapResponse {
let filter = filter
.into_iter()
@@ -577,15 +582,20 @@ impl Account {
})
})
.collect::<Vec<Value>>();
self.jmap_method_calls(json!([[
format!("{object}/query"),
{
"filter": filter,
"sort": sort_by
},
"0"
]]))
.await
let arguments = [
("filter".to_string(), Value::Object(filter)),
("sort".to_string(), Value::Array(sort_by)),
]
.into_iter()
.chain(
arguments
.into_iter()
.map(|(k, v)| (k.to_string(), v.into())),
)
.collect::<serde_json::Map<_, _>>();
self.jmap_method_calls(json!([[format!("{object}/query"), arguments, "0"]]))
.await
}
pub async fn jmap_create(
@@ -798,6 +808,33 @@ impl Account {
]))
.await;
}
pub async fn destroy_all_calendars(&self) {
self.jmap_method_calls(json!([[
"Calendar/get",
{
"ids" : (),
"properties" : [
"id"
]
},
"R1"
],
[
"Calendar/set",
{
"#destroy" : {
"resultOf": "R1",
"name": "Calendar/get",
"path": "/list/*/id"
},
"onDestroyRemoveEvents" : true
},
"R2"
]
]))
.await;
}
}
impl JmapResponse {
@@ -837,6 +874,12 @@ impl JmapResponse {
.unwrap_or_else(|| panic!("Missing method response in response: {self:?}"))
}
pub fn list_array(&self) -> &Value {
self.0
.pointer("/methodResponses/0/1/list")
.unwrap_or_else(|| panic!("Missing list in response: {self:?}"))
}
pub fn list(&self) -> &[Value] {
self.0
.pointer("/methodResponses/0/1/list")
@@ -933,6 +976,8 @@ pub trait JmapUtils {
self.text_field("description")
}
fn with_property(self, field: impl Display, value: impl Into<Value>) -> Self;
fn text_field(&self, field: &str) -> &str;
fn assert_is_equal(&self, other: Value);
@@ -953,6 +998,14 @@ impl JmapUtils for Value {
);
}
}
fn with_property(mut self, field: impl Display, value: impl Into<Value>) -> Self {
if let Value::Object(map) = &mut self {
map.insert(field.to_string(), value.into());
} else {
panic!("Not an object: {self:?}");
}
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]

View File

@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod availability;
pub mod get;