mod common;
use jmap_calendars_types::{
Alert, AlertTrigger, BusyPeriod, BusyStatus, Calendar, CalendarAlert, CalendarEvent,
CalendarEventFilterCondition, CalendarEventNotification, CalendarRights,
CalendarsAccountCapability, CalendarsCapability, IncludeInAvailability, JMAP_CALENDARS_URI,
Link, Participant, ParticipantIdentity, Person, PrincipalCalendarsCapability,
PrincipalsAvailabilityAccountCapability, VirtualLocation,
};
use jmap_types::Id;
#[test]
fn calendar_deserialize_full() {
let json = &common::fixture("calendar_deserialize_full.json");
let cal: Calendar = serde_json::from_str(json).expect("Calendar deserialize");
let id_val = cal.id.as_ref().expect("id present");
assert_eq!(id_val.as_ref(), "cal-001");
assert_eq!(cal.name, "Personal");
assert_eq!(cal.description.as_deref(), Some("My personal calendar"));
assert_eq!(cal.color.as_deref(), Some("#4287f5"));
assert_eq!(cal.sort_order, 5);
assert!(cal.is_subscribed);
assert!(cal.is_visible);
assert!(cal.is_default);
assert_eq!(cal.include_in_availability, IncludeInAvailability::All);
assert!(cal.default_alerts_with_time.is_none());
assert!(cal.default_alerts_without_time.is_none());
assert_eq!(cal.time_zone.as_deref(), Some("America/New_York"));
assert!(cal.share_with.is_none());
assert!(cal.my_rights.may_read_free_busy);
assert!(cal.my_rights.may_write_all);
assert!(cal.my_rights.may_share);
assert!(cal.my_rights.may_delete);
}
#[test]
fn calendar_roundtrip() {
let json = &common::fixture("calendar_roundtrip.json");
let original: Calendar = serde_json::from_str(json).expect("first deserialize");
let serialized = serde_json::to_string(&original).expect("serialize");
let recovered: Calendar = serde_json::from_str(&serialized).expect("second deserialize");
assert_eq!(original, recovered);
}
#[test]
fn calendar_id_absent_when_none() {
let json = &common::fixture("calendar_id_absent_when_none.json");
let cal: Calendar = serde_json::from_str(json).expect("Calendar must deserialize without id");
assert!(cal.id.is_none(), "id must be None when absent from input");
let out = serde_json::to_value(&cal).expect("serialize");
let obj = out.as_object().expect("object");
assert!(
!obj.contains_key("id"),
"id must be absent from wire when None (RFC 8620 §5.3 create payload contract): {out}"
);
}
#[test]
fn calendar_share_with() {
let json = &common::fixture("calendar_share_with.json");
let cal: Calendar = serde_json::from_str(json).expect("calendar with shareWith");
let share = cal.share_with.as_ref().expect("shareWith not None");
let rights = share.values().next().expect("at least one entry");
assert!(rights.may_read_free_busy);
assert!(rights.may_read_items);
assert!(!rights.may_write_all);
}
#[test]
fn calendar_rights_default_all_false() {
let r = CalendarRights::default();
assert!(!r.may_read_free_busy);
assert!(!r.may_read_items);
assert!(!r.may_write_all);
assert!(!r.may_write_own);
assert!(!r.may_update_private);
assert!(!r.may_rsvp);
assert!(!r.may_share);
assert!(!r.may_delete);
}
#[test]
fn calendar_rights_wire_names() {
let json = &common::fixture("calendar_rights_wire_names.json");
let rights: CalendarRights = serde_json::from_str(json).expect("deserialize rights");
let out = serde_json::to_value(&rights).expect("serialize");
assert_eq!(out["mayReadFreeBusy"], true);
assert_eq!(out["mayReadItems"], true);
assert_eq!(out["mayWriteAll"], true);
assert_eq!(out["mayWriteOwn"], true);
assert_eq!(out["mayUpdatePrivate"], true);
assert_eq!(out["mayRSVP"], true);
assert_eq!(out["mayShare"], true);
assert_eq!(out["mayDelete"], true);
}
#[test]
fn include_in_availability_known_values() {
let all = serde_json::to_string(&IncludeInAvailability::All).expect("serialize All");
let attending =
serde_json::to_string(&IncludeInAvailability::Attending).expect("serialize Attending");
let none = serde_json::to_string(&IncludeInAvailability::None).expect("serialize None");
assert_eq!(all, r#""all""#);
assert_eq!(attending, r#""attending""#);
assert_eq!(none, r#""none""#);
}
#[test]
fn include_in_availability_deserialize() {
let all: IncludeInAvailability = serde_json::from_str(r#""all""#).expect("all");
let attending: IncludeInAvailability =
serde_json::from_str(r#""attending""#).expect("attending");
let none: IncludeInAvailability = serde_json::from_str(r#""none""#).expect("none");
assert_eq!(all, IncludeInAvailability::All);
assert_eq!(attending, IncludeInAvailability::Attending);
assert_eq!(none, IncludeInAvailability::None);
}
#[test]
fn calendar_event_minimal() {
let json = &common::fixture("calendar_event_minimal.json");
let ev: CalendarEvent = serde_json::from_str(json).expect("minimal CalendarEvent");
let id_val = ev.id.as_ref().expect("id present");
assert_eq!(id_val.as_ref(), "event-001");
let cals = ev.calendar_ids.as_ref().expect("calendarIds");
assert_eq!(cals.len(), 1);
assert!(*cals.values().next().expect("first value"));
}
#[test]
fn calendar_event_all_none() {
let ev: CalendarEvent = serde_json::from_str("{}").expect("empty CalendarEvent");
assert!(ev.id.is_none());
assert!(ev.calendar_ids.is_none());
assert!(ev.title.is_none());
assert!(ev.start.is_none());
assert!(ev.recurrence_rules.is_none());
}
#[test]
fn calendar_event_is_draft_and_use_default_alerts_always_serialized() {
let ev: CalendarEvent = serde_json::from_str(&common::fixture(
"calendar_event_is_draft_and_use_default_alerts_always_serialized.json",
))
.unwrap();
let out = serde_json::to_value(&ev).unwrap();
assert_eq!(out["isDraft"], serde_json::Value::Bool(false), "{out}");
assert_eq!(
out["useDefaultAlerts"],
serde_json::Value::Bool(false),
"{out}"
);
}
#[test]
fn calendar_event_scalar_fields() {
let json = &common::fixture("calendar_event_scalar_fields.json");
let ev: CalendarEvent = serde_json::from_str(json).expect("CalendarEvent deserialize");
assert_eq!(ev.at_type.as_deref(), Some("Event"));
assert_eq!(
ev.uid.as_deref(),
Some("2a358cee-6489-4f14-a57f-c104db4dc357")
);
assert_eq!(ev.title.as_deref(), Some("Team meeting"));
assert_eq!(ev.description.as_deref(), Some("Monthly all-hands"));
assert_eq!(ev.start.as_deref(), Some("2024-06-15T09:00:00"));
assert_eq!(ev.duration.as_deref(), Some("PT1H"));
assert_eq!(ev.time_zone.as_deref(), Some("America/New_York"));
assert_eq!(ev.status.as_deref(), Some("confirmed"));
assert_eq!(ev.is_draft, Some(false));
assert_eq!(ev.is_origin, Some(true));
assert_eq!(ev.show_without_time, Some(false));
assert_eq!(ev.priority, Some(5));
assert_eq!(ev.free_busy_status.as_deref(), Some("busy"));
assert_eq!(ev.privacy.as_deref(), Some("public"));
assert_eq!(ev.sequence, Some(0));
assert_eq!(ev.color.as_deref(), Some("red"));
}
#[test]
fn calendar_event_keywords_categories_reply_to_typed() {
let ev: CalendarEvent = serde_json::from_str(
r#"{
"keywords": { "work": true },
"categories": { "http://example.com/cat/meeting": true },
"replyTo": { "imip": "mailto:organizer@example.com" }
}"#,
)
.expect("CalendarEvent deserialize");
assert_eq!(
ev.keywords.as_ref().and_then(|m| m.get("work")),
Some(&true)
);
assert_eq!(
ev.categories
.as_ref()
.and_then(|m| m.get("http://example.com/cat/meeting")),
Some(&true)
);
assert_eq!(
ev.reply_to
.as_ref()
.and_then(|m| m.get("imip"))
.map(String::as_str),
Some("mailto:organizer@example.com")
);
let bad = serde_json::from_str::<CalendarEvent>(r#"{ "keywords": { "work": "yes" } }"#);
assert!(bad.is_err(), "non-Boolean keyword value must be rejected");
}
#[test]
fn calendar_event_recurrence_overrides_passthrough() {
let json = &common::fixture("calendar_event_recurrence_overrides_passthrough.json");
let ev: CalendarEvent = serde_json::from_str(json).expect("recurrence CalendarEvent");
let overrides = ev
.recurrence_overrides
.as_ref()
.expect("recurrenceOverrides");
let patch = overrides
.get("2024-06-22T09:00:00")
.expect("override key present");
assert_eq!(
patch.as_map().get("title"),
Some(&serde_json::Value::String("Rescheduled meeting".to_owned()))
);
}
#[test]
fn calendar_event_recurrence_overrides_wire_format_is_object_map() {
let json =
&common::fixture("calendar_event_recurrence_overrides_wire_format_is_object_map.json");
let ev: CalendarEvent = serde_json::from_str(json).expect("deserialize");
let out: serde_json::Value = serde_json::to_value(&ev).expect("serialize");
let expected: serde_json::Value = serde_json::from_str(json).expect("expected as Value");
assert_eq!(
out["recurrenceOverrides"], expected["recurrenceOverrides"],
"wire format must be byte-identical for object-map input"
);
}
#[test]
fn calendar_event_time_zones_value_and_typed_roundtrip() {
use jmap_calendars_types::TimeZone;
let json = &common::fixture("calendar_event_time_zones_value_and_typed_roundtrip.json");
let ev: CalendarEvent = serde_json::from_str(json).expect("CalendarEvent must deserialize");
let tz_field = ev.time_zones.as_ref().expect("timeZones must be Some");
let tz_map = tz_field
.as_object()
.expect("timeZones must be a JSON object");
let entry = tz_map
.get("/example/custom/UTC+05:30:00")
.expect("custom tz entry");
let tz: TimeZone = serde_json::from_value(entry.clone()).expect("typed TimeZone decode");
assert_eq!(tz.at_type, "TimeZone");
assert_eq!(tz.tz_id, "/example/custom/UTC+05:30:00");
let standard = tz.standard.as_ref().expect("standard rule present");
assert_eq!(standard.len(), 1);
assert_eq!(standard[0].at_type, "TimeZoneRule");
assert_eq!(standard[0].offset_from.as_ref(), "+0530");
assert_eq!(standard[0].offset_to.as_ref(), "+0530");
let out: serde_json::Value = serde_json::to_value(&ev).expect("serialize");
let expected: serde_json::Value = serde_json::from_str(json).expect("expected Value");
assert_eq!(
out["timeZones"], expected["timeZones"],
"wire format must be byte-identical"
);
}
#[test]
fn calendar_event_localizations_passthrough() {
let json = &common::fixture("calendar_event_localizations_passthrough.json");
let ev: CalendarEvent = serde_json::from_str(json).expect("localizations CalendarEvent");
let locs = ev.localizations.as_ref().expect("localizations");
let fr = locs.get("fr").expect("fr override");
assert_eq!(
fr.as_map().get("title"),
Some(&serde_json::Value::String("Déjeuner".to_owned()))
);
let es = locs.get("es").expect("es override");
assert_eq!(
es.as_map().get("title"),
Some(&serde_json::Value::String("Almuerzo".to_owned()))
);
let out: serde_json::Value = serde_json::to_value(&ev).expect("serialize");
let expected: serde_json::Value = serde_json::from_str(json).expect("expected as Value");
assert_eq!(out["localizations"], expected["localizations"]);
}
#[test]
fn calendar_event_roundtrip() {
let json = &common::fixture("calendar_event_roundtrip.json");
let original: CalendarEvent = serde_json::from_str(json).expect("first deserialize");
let serialized = serde_json::to_string(&original).expect("serialize");
let recovered: CalendarEvent = serde_json::from_str(&serialized).expect("second deserialize");
assert_eq!(original, recovered);
}
#[test]
fn calendar_event_filter_condition_empty() {
let fc: CalendarEventFilterCondition =
serde_json::from_str("{}").expect("empty FilterCondition");
assert!(fc.in_calendar.is_none());
assert!(fc.after.is_none());
assert!(fc.before.is_none());
assert!(fc.text.is_none());
assert!(fc.title.is_none());
assert!(fc.description.is_none());
assert!(fc.location.is_none());
assert!(fc.owner.is_none());
assert!(fc.participant.is_none());
assert!(fc.uid.is_none());
}
#[test]
fn calendar_event_filter_condition_wire_names() {
let json = &common::fixture("calendar_event_filter_condition_wire_names.json");
let fc: CalendarEventFilterCondition = serde_json::from_str(json).expect("filter condition");
assert_eq!(
fc.in_calendar.as_ref().map(|id| id.as_ref()),
Some("cal-001")
);
assert_eq!(
fc.after.as_ref().map(AsRef::as_ref),
Some("2024-01-01T00:00:00")
);
assert_eq!(
fc.before.as_ref().map(AsRef::as_ref),
Some("2024-12-31T23:59:59")
);
assert_eq!(fc.text.as_deref(), Some("meeting"));
assert_eq!(fc.title.as_deref(), Some("team"));
assert!(fc.description.is_none());
assert!(fc.owner.is_none());
let out = serde_json::to_value(&fc).expect("serialize");
assert_eq!(out["inCalendar"], "cal-001");
assert_eq!(out["after"], "2024-01-01T00:00:00");
assert_eq!(out["before"], "2024-12-31T23:59:59");
}
#[test]
fn calendar_event_filter_condition_participant_replaces_attendee() {
let fc: CalendarEventFilterCondition = serde_json::from_str(&common::fixture(
"calendar_event_filter_condition_participant_replaces_attendee.json",
))
.expect("participant condition");
assert_eq!(fc.participant.as_deref(), Some("tom@foobar.example.com"));
assert_eq!(
serde_json::to_value(&fc).expect("serialize"),
serde_json::json!({ "participant": "tom@foobar.example.com" })
);
assert!(
serde_json::from_str::<CalendarEventFilterCondition>(&common::fixture(
"calendar_event_filter_condition_participant_replaces_attendee_2.json"
))
.is_err(),
"the draft-26 `attendee` condition must no longer parse"
);
}
#[test]
fn notification_deserialize() {
let json = &common::fixture("notification_deserialize.json");
let n: CalendarEventNotification =
serde_json::from_str(json).expect("CalendarEventNotification deserialize");
assert_eq!(n.id.as_ref(), "notif-001");
assert_eq!(n.created, "2024-06-15T10:05:00Z");
assert_eq!(n.changed_by.name, "Alice Smith");
assert_eq!(n.changed_by.email.as_deref(), Some("alice@example.com"));
assert_eq!(
n.changed_by.principal_id.as_ref().map(|id| id.as_ref()),
Some("principal-alice")
);
assert_eq!(
n.changed_by.calendar_address.as_deref(),
Some("mailto:alice@example.com")
);
assert!(n.comment.is_none());
assert_eq!(
n.notification_type,
jmap_calendars_types::NotificationType::Updated
);
assert_eq!(n.calendar_event_id.as_ref(), "event-001");
assert_eq!(n.is_draft, Some(false));
assert_eq!(n.event["title"], "Original Title");
let patch = n.event_patch.as_ref().expect("eventPatch");
assert_eq!(
patch.as_map().get("title"),
Some(&serde_json::Value::String("New Title".to_owned()))
);
let out: serde_json::Value = serde_json::to_value(&n).expect("serialize notification");
assert_eq!(out["eventPatch"]["title"], "New Title");
}
#[test]
fn notification_created_no_patch() {
let json = &common::fixture("notification_created_no_patch.json");
let n: CalendarEventNotification = serde_json::from_str(json).expect("created notification");
assert_eq!(
n.notification_type,
jmap_calendars_types::NotificationType::Created
);
assert!(n.event_patch.is_none());
assert_eq!(n.is_draft, Some(true));
}
#[test]
fn notification_type_known_values() {
use jmap_calendars_types::NotificationType;
let created = serde_json::to_string(&NotificationType::Created).expect("created");
let updated = serde_json::to_string(&NotificationType::Updated).expect("updated");
let destroyed = serde_json::to_string(&NotificationType::Destroyed).expect("destroyed");
assert_eq!(created, r#""created""#);
assert_eq!(updated, r#""updated""#);
assert_eq!(destroyed, r#""destroyed""#);
}
#[test]
fn notification_type_other_roundtrip() {
use jmap_calendars_types::NotificationType;
let other: NotificationType = serde_json::from_str(r#""rescheduled""#).expect("other type");
match &other {
NotificationType::Other(s) => assert_eq!(s, "rescheduled"),
_ => panic!("expected Other variant"),
}
let back = serde_json::to_string(&other).expect("re-serialize");
assert_eq!(back, r#""rescheduled""#);
}
#[test]
fn notification_filter_type_is_notification_type() {
use jmap_calendars_types::{NotificationFilterCondition, NotificationType};
let known: NotificationFilterCondition = serde_json::from_str(&common::fixture(
"notification_filter_type_is_notification_type.json",
))
.expect("known type");
assert_eq!(known.notification_type, Some(NotificationType::Updated));
let unknown: NotificationFilterCondition = serde_json::from_str(&common::fixture(
"notification_filter_type_is_notification_type_2.json",
))
.expect("unknown type");
assert_eq!(
serde_json::to_value(&unknown).expect("serialize"),
serde_json::json!({"type": "rescheduled"})
);
}
#[test]
fn participant_identity_roundtrip() {
let json = &common::fixture("participant_identity_roundtrip.json");
let pi: ParticipantIdentity = serde_json::from_str(json).expect("ParticipantIdentity");
let id_val = pi.id.as_ref().expect("id present");
assert_eq!(id_val.as_ref(), "pi-001");
assert_eq!(pi.name, "Joe Bloggs");
assert_eq!(pi.calendar_address, "mailto:joe@example.com");
assert!(pi.is_default);
let serialized = serde_json::to_string(&pi).expect("serialize");
let recovered: ParticipantIdentity = serde_json::from_str(&serialized).expect("re-deserialize");
assert_eq!(pi, recovered);
}
#[test]
fn participant_identity_wire_names() {
let json = &common::fixture("participant_identity_wire_names.json");
let pi: ParticipantIdentity = serde_json::from_str(json).expect("ParticipantIdentity");
let out = serde_json::to_value(&pi).expect("serialize");
assert_eq!(out["id"], "pi-002");
assert_eq!(out["name"], "Jane Doe");
assert_eq!(out["calendarAddress"], "mailto:jane@example.com");
assert_eq!(out["isDefault"], false);
}
#[test]
fn participant_identity_id_absent_when_none() {
let json = &common::fixture("participant_identity_id_absent_when_none.json");
let pi: ParticipantIdentity =
serde_json::from_str(json).expect("ParticipantIdentity must deserialize without id");
assert!(pi.id.is_none(), "id must be None when absent from input");
let out = serde_json::to_value(&pi).expect("serialize");
let obj = out.as_object().expect("object");
assert!(
!obj.contains_key("id"),
"id must be absent from wire when None (RFC 8620 §5.3 create payload contract): {out}"
);
}
#[test]
fn calendars_capability_empty_object() {
let cap = CalendarsCapability::default();
let json = serde_json::to_string(&cap).expect("serialize");
assert_eq!(json, "{}");
}
#[test]
fn calendars_account_capability_roundtrip() {
let json = &common::fixture("calendars_account_capability_roundtrip.json");
let cap: CalendarsAccountCapability =
serde_json::from_str(json).expect("CalendarsAccountCapability");
assert_eq!(cap.max_calendars_per_event, Some(5));
assert_eq!(cap.min_date_time, "1970-01-01T00:00:00Z");
assert_eq!(cap.max_date_time, "2099-12-31T23:59:59Z");
assert_eq!(cap.max_expanded_query_duration.as_ref(), "P1Y");
assert!(cap.max_participants_per_event.is_none());
assert!(cap.may_create_calendar);
let s = serde_json::to_string(&cap).expect("serialize");
let r: CalendarsAccountCapability = serde_json::from_str(&s).expect("re-deserialize");
assert_eq!(cap, r);
}
#[test]
fn principals_availability_account_capability_roundtrip() {
let json = &common::fixture("principals_availability_account_capability_roundtrip.json");
let cap: PrincipalsAvailabilityAccountCapability =
serde_json::from_str(json).expect("must deserialize");
assert_eq!(cap.max_availability_duration.as_ref(), "P56D");
assert_eq!(serde_json::to_string(&cap).expect("serialize"), *json);
}
#[test]
fn calendars_uri_constant() {
assert_eq!(JMAP_CALENDARS_URI, "urn:ietf:params:jmap:calendars");
}
#[test]
fn alert_offset_trigger_roundtrip() {
let json = &common::fixture("alert_offset_trigger_roundtrip.json");
let trigger: AlertTrigger = serde_json::from_str(json).expect("OffsetTrigger");
match &trigger {
AlertTrigger::OffsetTrigger(t) => {
assert_eq!(t.at_type, "OffsetTrigger");
assert_eq!(t.offset.as_ref(), "-PT15M");
assert_eq!(t.relative_to.as_deref(), Some("start"));
}
_ => panic!("expected OffsetTrigger variant"),
}
let back = serde_json::to_string(&trigger).expect("serialize");
let recovered: AlertTrigger = serde_json::from_str(&back).expect("re-deserialize");
assert_eq!(trigger, recovered);
}
#[test]
fn alert_absolute_trigger_roundtrip() {
let json = &common::fixture("alert_absolute_trigger_roundtrip.json");
let trigger: AlertTrigger = serde_json::from_str(json).expect("AbsoluteTrigger");
match &trigger {
AlertTrigger::AbsoluteTrigger(t) => {
assert_eq!(t.at_type, "AbsoluteTrigger");
assert_eq!(t.when, "2024-06-15T08:45:00Z");
}
_ => panic!("expected AbsoluteTrigger variant"),
}
let back = serde_json::to_string(&trigger).expect("serialize");
let recovered: AlertTrigger = serde_json::from_str(&back).expect("re-deserialize");
assert_eq!(trigger, recovered);
}
#[test]
fn alert_unknown_trigger_roundtrip() {
let json = &common::fixture("alert_unknown_trigger_roundtrip.json");
let trigger: AlertTrigger = serde_json::from_str(json).expect("unknown trigger");
match &trigger {
AlertTrigger::Unknown(v) => {
assert_eq!(v["@type"], "CustomTrigger");
assert_eq!(v["customField"], "customValue");
}
_ => panic!("expected Unknown variant"),
}
let back = serde_json::to_string(&trigger).expect("re-serialize");
let recovered: AlertTrigger = serde_json::from_str(&back).expect("re-deserialize");
match &recovered {
AlertTrigger::Unknown(v) => assert_eq!(v["@type"], "CustomTrigger"),
_ => panic!("round-trip should remain Unknown"),
}
}
#[test]
fn alert_with_offset_trigger() {
let json = &common::fixture("alert_with_offset_trigger.json");
let alert: Alert = serde_json::from_str(json).expect("Alert");
assert_eq!(alert.at_type, "Alert");
assert_eq!(alert.action.as_deref(), Some("display"));
assert!(alert.acknowledged.is_none());
match &alert.trigger {
AlertTrigger::OffsetTrigger(t) => assert_eq!(t.offset.as_ref(), "-PT15M"),
_ => panic!("expected OffsetTrigger"),
}
}
#[test]
fn person_wire_names() {
let json = &common::fixture("person_wire_names.json");
let p: Person = serde_json::from_str(json).expect("Person");
let out = serde_json::to_value(&p).expect("serialize");
assert_eq!(out["name"], "Charlie Brown");
assert_eq!(out["email"], "charlie@example.com");
assert_eq!(out["principalId"], "princ-007");
assert_eq!(out["calendarAddress"], "mailto:charlie@example.com");
}
#[test]
fn person_null_fields() {
let json = &common::fixture("person_null_fields.json");
let p: Person = serde_json::from_str(json).expect("Person");
assert_eq!(p.name, "Eve");
assert!(p.email.is_none());
assert!(p.principal_id.is_none());
assert!(p.calendar_address.is_none());
}
#[test]
fn busy_period_full_deserialize() {
let json = &common::fixture("busy_period_full_deserialize.json");
let bp: BusyPeriod = serde_json::from_str(json).expect("BusyPeriod deserialize");
assert_eq!(bp.utc_start, "2024-06-15T09:00:00Z");
assert_eq!(bp.utc_end, "2024-06-15T10:00:00Z");
assert_eq!(bp.busy_status, BusyStatus::Confirmed);
assert!(bp.event.is_some(), "event must be deserialized");
assert_eq!(bp.event.as_ref().unwrap()["title"], "Team meeting");
assert!(bp.account_id.is_some(), "accountId must be present");
assert_eq!(bp.account_id.as_ref().unwrap().as_ref(), "acc-42");
}
#[test]
fn busy_period_no_detail_access() {
let json = &common::fixture("busy_period_no_detail_access.json");
let bp: BusyPeriod = serde_json::from_str(json).expect("BusyPeriod minimal deserialize");
assert_eq!(bp.utc_start, "2024-06-15T14:00:00Z");
assert_eq!(bp.utc_end, "2024-06-15T15:30:00Z");
assert_eq!(bp.busy_status, BusyStatus::Unavailable);
assert!(bp.event.is_none());
assert!(bp.account_id.is_none());
let v = serde_json::to_value(&bp).expect("serialize");
let obj = v.as_object().expect("object");
assert!(!obj.contains_key("event"), "event must be absent when None");
assert!(
!obj.contains_key("accountId"),
"accountId must be absent when None"
);
}
#[test]
fn busy_period_roundtrip() {
let json = &common::fixture("busy_period_roundtrip.json");
let original: BusyPeriod = serde_json::from_str(json).expect("first deserialize");
let serialized = serde_json::to_string(&original).expect("serialize");
let back: BusyPeriod = serde_json::from_str(&serialized).expect("re-deserialize");
assert_eq!(original.utc_start, back.utc_start);
assert_eq!(original.utc_end, back.utc_end);
assert_eq!(original.busy_status, back.busy_status);
assert_eq!(original.account_id, back.account_id);
assert_eq!(original.event, back.event);
}
#[test]
fn busy_period_wire_names() {
let json = &common::fixture("busy_period_wire_names.json");
let bp: BusyPeriod = serde_json::from_str(json).expect("deserialize");
let v = serde_json::to_value(&bp).expect("serialize");
let obj = v.as_object().expect("object");
assert!(obj.contains_key("utcStart"), "wire key must be utcStart");
assert!(obj.contains_key("utcEnd"), "wire key must be utcEnd");
assert!(
obj.contains_key("busyStatus"),
"wire key must be busyStatus"
);
assert!(obj.contains_key("accountId"), "wire key must be accountId");
}
#[test]
fn busy_period_absent_busy_status_defaults_to_unavailable() {
let json = &common::fixture("busy_period_absent_busy_status_defaults_to_unavailable.json");
let bp: BusyPeriod = serde_json::from_str(json).expect("deserialize");
assert_eq!(bp.busy_status, BusyStatus::Unavailable);
}
#[test]
fn busy_period_rejects_unknown_busy_status() {
let json = &common::fixture("busy_period_rejects_unknown_busy_status.json");
assert!(serde_json::from_str::<BusyPeriod>(json).is_err());
}
#[test]
fn busy_status_wire_strings() {
for (v, wire) in [
(BusyStatus::Confirmed, "confirmed"),
(BusyStatus::Tentative, "tentative"),
(BusyStatus::Unavailable, "unavailable"),
] {
assert_eq!(serde_json::to_value(v).unwrap(), serde_json::json!(wire));
}
}
#[test]
fn principal_calendars_capability_with_account_id() {
let json = &common::fixture("principal_calendars_capability_with_account_id.json");
let cap: PrincipalCalendarsCapability =
serde_json::from_str(json).expect("PrincipalCalendarsCapability deserialize");
assert_eq!(
cap.account_id.as_ref().map(|id| id.as_ref()),
Some("acc-alice")
);
assert!(cap.may_get_availability);
assert!(cap.may_share_with);
assert_eq!(
cap.calendar_address.as_deref(),
Some("mailto:alice@example.com")
);
}
#[test]
fn principal_calendars_capability_calendar_address_null() {
let wire = serde_json::json!({
"accountId": null,
"mayGetAvailability": true,
"mayShareWith": false,
"calendarAddress": null
});
let cap: PrincipalCalendarsCapability =
serde_json::from_value(wire.clone()).expect("null calendarAddress must parse");
assert!(cap.calendar_address.is_none());
assert_eq!(serde_json::to_value(&cap).expect("serialize"), wire);
}
#[test]
fn principal_calendars_capability_account_id_null() {
let json = &common::fixture("principal_calendars_capability_account_id_null.json");
let cap: PrincipalCalendarsCapability =
serde_json::from_str(json).expect("deserialize with null accountId");
assert!(cap.account_id.is_none(), "accountId must be None for null");
assert!(!cap.may_get_availability);
assert!(!cap.may_share_with);
}
#[test]
fn principal_calendars_capability_account_id_null_serializes() {
let json = &common::fixture("principal_calendars_capability_account_id_null_serializes.json");
let cap: PrincipalCalendarsCapability = serde_json::from_str(json).expect("deserialize");
let v = serde_json::to_value(&cap).expect("serialize");
let obj = v.as_object().expect("object");
assert!(
obj.contains_key("accountId"),
"accountId must be present in wire JSON (required-nullable)"
);
assert!(
obj["accountId"].is_null(),
"accountId must serialize as null"
);
}
#[test]
fn principal_calendars_capability_roundtrip() {
let json = &common::fixture("principal_calendars_capability_roundtrip.json");
let original: PrincipalCalendarsCapability =
serde_json::from_str(json).expect("first deserialize");
let serialized = serde_json::to_string(&original).expect("serialize");
let back: PrincipalCalendarsCapability =
serde_json::from_str(&serialized).expect("re-deserialize");
assert_eq!(original.account_id, back.account_id);
assert_eq!(original.may_get_availability, back.may_get_availability);
assert_eq!(original.may_share_with, back.may_share_with);
assert_eq!(original.calendar_address, back.calendar_address);
}
#[test]
fn participant_schedule_fields_roundtrip() {
let json = &common::fixture("participant_schedule_fields_roundtrip.json");
let p: Participant = serde_json::from_str(json).expect("Participant deserialize");
assert_eq!(p.schedule_sequence, Some(3));
assert_eq!(
p.schedule_updated.as_ref().map(AsRef::as_ref),
Some("2024-06-15T10:30:00Z")
);
let serialized = serde_json::to_string(&p).expect("serialize");
let back: Participant = serde_json::from_str(&serialized).expect("re-deserialize");
assert_eq!(back.schedule_sequence, Some(3));
assert_eq!(
back.schedule_updated.as_ref().map(AsRef::as_ref),
Some("2024-06-15T10:30:00Z")
);
let v: serde_json::Value = serde_json::from_str(&serialized).expect("parse");
assert_eq!(v["scheduleSequence"], serde_json::json!(3_u64));
assert_eq!(
v["scheduleUpdated"],
serde_json::json!("2024-06-15T10:30:00Z")
);
}
#[test]
fn participant_schedule_fields_absent_when_none() {
let json = &common::fixture("participant_schedule_fields_absent_when_none.json");
let p: Participant = serde_json::from_str(json).expect("Participant deserialize");
assert_eq!(p.schedule_sequence, None);
assert_eq!(p.schedule_updated, None);
assert!(
p.schedule_status.is_none(),
"scheduleStatus must be None when absent"
);
let serialized = serde_json::to_string(&p).expect("serialize");
let v: serde_json::Value = serde_json::from_str(&serialized).expect("parse");
let obj = v.as_object().expect("object");
assert!(
!obj.contains_key("scheduleSequence"),
"scheduleSequence must be absent when None"
);
assert!(
!obj.contains_key("scheduleUpdated"),
"scheduleUpdated must be absent when None"
);
assert!(
!obj.contains_key("scheduleStatus"),
"scheduleStatus must be absent when None"
);
}
#[test]
fn id_from_str_works() {
let id = Id::from("test-id-123");
assert_eq!(id.as_ref(), "test-id-123");
}
#[test]
fn link_with_href_roundtrip() {
let json = &common::fixture("link_with_href_roundtrip.json");
let link: Link =
serde_json::from_str(json).expect("link_with_href_roundtrip: must deserialize");
assert_eq!(link.at_type, "Link");
assert_eq!(link.href.as_deref(), Some("https://example.com/file.pdf"));
assert_eq!(link.content_type.as_deref(), Some("application/pdf"));
assert_eq!(link.size, Some(4096));
assert_eq!(link.rel.as_deref(), Some("enclosure"));
assert_eq!(link.display.as_deref(), Some("report.pdf"));
assert!(
link.blob_id.is_none(),
"blob_id must be absent when not in JSON"
);
let out = serde_json::to_string(&link).expect("link_with_href_roundtrip: must serialize");
assert!(
!out.contains("blobId"),
"blobId must not appear when None: {out}"
);
}
#[test]
fn link_with_blob_id_roundtrip() {
let json = &common::fixture("link_with_blob_id_roundtrip.json");
let link: Link =
serde_json::from_str(json).expect("link_with_blob_id_roundtrip: must deserialize");
assert_eq!(link.blob_id, Some(Id::from("blob-abc123")));
assert!(link.href.is_none(), "href must be absent when not in JSON");
let out = serde_json::to_value(&link).expect("link_with_blob_id_roundtrip: must serialize");
assert_eq!(
out["blobId"], "blob-abc123",
"blobId must serialize to wire name"
);
assert!(
out.get("href").is_none() || out["href"].is_null(),
"href must be absent when None"
);
}
#[test]
fn link_blob_id_absent_when_none() {
let json = &common::fixture("link_blob_id_absent_when_none.json");
let link: Link =
serde_json::from_str(json).expect("link_blob_id_absent_when_none: must deserialize");
assert!(
link.blob_id.is_none(),
"blob_id must be None when absent from JSON"
);
let out = serde_json::to_string(&link).expect("link_blob_id_absent_when_none: must serialize");
assert!(
!out.contains("blobId"),
"blobId must not appear when None: {out}"
);
}
#[test]
fn link_wire_names() {
let json = &common::fixture("link_wire_names.json");
let link: Link = serde_json::from_str(json).expect("link_wire_names: must deserialize");
let out = serde_json::to_value(&link).expect("link_wire_names: must serialize");
assert!(
out.get("blobId").is_some(),
"blobId must be camelCase wire key"
);
assert!(
out.get("contentType").is_some(),
"contentType must be camelCase wire key"
);
assert!(out.get("rel").is_some(), "rel must be present");
assert!(out.get("display").is_some(), "display must be present");
assert!(
out.get("blob_id").is_none(),
"snake_case blob_id must not appear on wire"
);
assert!(
out.get("content_type").is_none(),
"snake_case content_type must not appear on wire"
);
}
#[test]
fn calendar_alert_with_recurrence_id_roundtrip() {
let json = &common::fixture("calendar_alert_with_recurrence_id_roundtrip.json");
let alert: CalendarAlert = serde_json::from_str(json)
.expect("calendar_alert_with_recurrence_id_roundtrip: must deserialize");
assert_eq!(
alert.at_type, "CalendarAlert",
"@type field must be 'CalendarAlert'"
);
assert_eq!(alert.account_id.as_ref(), "acc-1");
assert_eq!(alert.calendar_event_id.as_ref(), "ev-42");
assert_eq!(alert.uid, "abc-uid-123");
assert_eq!(
alert.recurrence_id.as_deref(),
Some("2024-06-15T10:00:00"),
"recurrenceId must be present for recurring events"
);
assert_eq!(alert.alert_id, "alert-1");
let serialized = serde_json::to_string(&alert)
.expect("calendar_alert_with_recurrence_id_roundtrip: must serialize");
let recovered: CalendarAlert = serde_json::from_str(&serialized)
.expect("calendar_alert_with_recurrence_id_roundtrip: round-trip must deserialize");
assert_eq!(alert, recovered);
}
#[test]
fn calendar_alert_recurrence_id_null_for_non_recurring() {
let json = &common::fixture("calendar_alert_recurrence_id_null_for_non_recurring.json");
let alert: CalendarAlert = serde_json::from_str(json)
.expect("calendar_alert_recurrence_id_null_for_non_recurring: must deserialize");
assert!(
alert.recurrence_id.is_none(),
"recurrenceId must be None when null"
);
let out = serde_json::to_value(&alert)
.expect("calendar_alert_recurrence_id_null_for_non_recurring: must serialize");
assert!(
out.get("recurrenceId").is_some(),
"recurrenceId key must be present in serialized output (not omitted)"
);
assert!(
out["recurrenceId"].is_null(),
"recurrenceId must serialize as null for non-recurring events"
);
}
#[test]
fn calendar_alert_wire_names() {
let json = &common::fixture("calendar_alert_wire_names.json");
let alert: CalendarAlert =
serde_json::from_str(json).expect("calendar_alert_wire_names: must deserialize");
let out = serde_json::to_value(&alert).expect("calendar_alert_wire_names: must serialize");
assert!(out.get("@type").is_some(), "@type must be present");
assert!(
out.get("accountId").is_some(),
"accountId must be camelCase"
);
assert!(
out.get("calendarEventId").is_some(),
"calendarEventId must be camelCase"
);
assert!(out.get("alertId").is_some(), "alertId must be camelCase");
assert!(
out.get("account_id").is_none(),
"account_id must not appear on wire"
);
assert!(
out.get("calendar_event_id").is_none(),
"calendar_event_id must not appear on wire"
);
assert!(
out.get("alert_id").is_none(),
"alert_id must not appear on wire"
);
assert!(
out.get("at_type").is_none(),
"at_type must not appear on wire (must be @type)"
);
}
#[test]
fn virtual_location_uri_required() {
let json = &common::fixture("virtual_location_uri_required.json");
let vl: VirtualLocation =
serde_json::from_str(json).expect("virtual_location_uri_required: must deserialize");
assert_eq!(
vl.uri, "https://meet.example.com/room-42",
"uri must round-trip correctly"
);
assert_eq!(vl.name.as_deref(), Some("Team Call"));
}
#[test]
fn virtual_location_uri_missing_fails_deserialization() {
let json = &common::fixture("virtual_location_uri_missing_fails_deserialization.json");
let result: Result<VirtualLocation, _> = serde_json::from_str(json);
assert!(
result.is_err(),
"VirtualLocation without uri must fail deserialization (uri is mandatory per RFC 8984 §4.2.6)"
);
}
#[test]
fn virtual_location_wire_names() {
let json = &common::fixture("virtual_location_wire_names.json");
let vl: VirtualLocation =
serde_json::from_str(json).expect("virtual_location_wire_names: must deserialize");
let out = serde_json::to_value(&vl).expect("virtual_location_wire_names: must serialize");
assert!(out.get("uri").is_some(), "uri must be present on wire");
assert!(out.get("@type").is_some(), "@type must be present on wire");
assert!(
out.get("at_type").is_none(),
"at_type must not appear on wire"
);
}
#[test]
fn link_cid_roundtrip() {
let json = &common::fixture("link_cid_roundtrip.json");
let link: Link = serde_json::from_str(json).expect("link_cid_roundtrip: must deserialize");
assert_eq!(
link.cid.as_deref(),
Some("logo@example.com"),
"cid must round-trip correctly"
);
let out = serde_json::to_value(&link).expect("link_cid_roundtrip: must serialize");
assert_eq!(
out["cid"], "logo@example.com",
"cid wire name must be 'cid'"
);
}
#[test]
fn link_cid_absent_when_none() {
let json = &common::fixture("link_blob_id_absent_when_none.json");
let link: Link =
serde_json::from_str(json).expect("link_cid_absent_when_none: must deserialize");
assert!(link.cid.is_none(), "cid must be None when absent from JSON");
let out = serde_json::to_string(&link).expect("link_cid_absent_when_none: must serialize");
assert!(
!out.contains("\"cid\""),
"cid must not appear in output when None: {out}"
);
}
#[test]
fn link_title_roundtrip() {
let json = &common::fixture("link_title_roundtrip.json");
let link: Link = serde_json::from_str(json).expect("link_title_roundtrip: must deserialize");
assert_eq!(
link.title.as_deref(),
Some("Annual Report 2024"),
"title must round-trip correctly"
);
let out = serde_json::to_value(&link).expect("link_title_roundtrip: must serialize");
assert_eq!(
out["title"], "Annual Report 2024",
"title wire name must be 'title'"
);
}
#[test]
fn link_title_absent_when_none() {
let json = &common::fixture("link_title_absent_when_none.json");
let link: Link =
serde_json::from_str(json).expect("link_title_absent_when_none: must deserialize");
assert!(
link.title.is_none(),
"title must be None when absent from JSON"
);
let out = serde_json::to_string(&link).expect("link_title_absent_when_none: must serialize");
assert!(
!out.contains("\"title\""),
"title must not appear in output when None: {out}"
);
}
#[test]
fn participant_schedule_status_roundtrip() {
let json = &common::fixture("participant_schedule_status_roundtrip.json");
let p: Participant = serde_json::from_str(json)
.expect("participant_schedule_status_roundtrip: must deserialize");
let status = p
.schedule_status
.as_ref()
.expect("scheduleStatus must be Some");
assert_eq!(status.len(), 2, "must have 2 status codes");
assert_eq!(status[0], "1.0");
assert_eq!(status[1], "2.0");
let out =
serde_json::to_value(&p).expect("participant_schedule_status_roundtrip: must serialize");
assert_eq!(
out["scheduleStatus"][0], "1.0",
"scheduleStatus[0] must be '1.0'"
);
assert_eq!(
out["scheduleStatus"][1], "2.0",
"scheduleStatus[1] must be '2.0'"
);
}
#[test]
fn calendar_event_icalendar_roundtrip() {
let ical = serde_json::json!({
"@type": "ICalComponent",
"name": "participant",
"properties": [
["uid", {}, "text", "47AD2E1C-49D4-45DF-BD83-8398ACC7D8E2"]
]
});
let wire = serde_json::json!({ "id": "ev1", "iCalendar": ical });
let event: CalendarEvent = serde_json::from_value(wire.clone()).expect("must deserialize");
assert_eq!(event.icalendar.as_ref(), Some(&ical));
assert!(
event.extra.is_empty(),
"iCalendar must land in the typed field"
);
let out = serde_json::to_value(&event).expect("must serialize");
assert_eq!(out["iCalendar"], ical);
}
#[test]
fn calendar_event_draft26_icalcomponent_lands_in_extras() {
let payload = &common::fixture("calendar_event_draft26_icalcomponent_lands_in_extras.json");
let ev: CalendarEvent = serde_json::from_str(payload).expect("must still deserialize");
assert!(ev.icalendar.is_none());
assert_eq!(
ev.extra.get("iCalComponent").and_then(|v| v.as_str()),
Some("QkVHSU46VkVWRU5U")
);
let reser = serde_json::to_value(&ev).expect("re-serialize must succeed");
assert_eq!(reser["iCalComponent"], "QkVHSU46VkVWRU5U");
assert!(reser.get("iCalendar").is_none());
}
#[test]
fn calendar_event_icalendar_absent_when_none() {
let event: CalendarEvent = serde_json::from_str(&common::fixture(
"calendar_event_icalendar_absent_when_none.json",
))
.expect("must deserialize");
assert!(event.icalendar.is_none());
let out = serde_json::to_value(&event).expect("must serialize");
assert!(out.get("iCalendar").is_none(), "{out}");
}
#[test]
fn calendar_event_version_is_typed() {
let wire = serde_json::json!({ "id": "E-5d5776f6", "@type": "Event", "version": "2.0" });
let event: CalendarEvent = serde_json::from_value(wire).expect("must deserialize");
assert_eq!(event.version.as_deref(), Some("2.0"));
assert!(event.extra.is_empty());
assert_eq!(
serde_json::to_value(&event).expect("serialize")["version"],
"2.0"
);
}
#[test]
fn busy_period_preserves_vendor_extras() {
let raw = serde_json::json!({
"utcStart": "2024-06-01T09:00:00Z",
"utcEnd": "2024-06-01T10:00:00Z",
"acmeCorpFreeBusyTag": "client-meeting"
});
let b: BusyPeriod = serde_json::from_value(raw).unwrap();
assert_eq!(
b.extra.get("acmeCorpFreeBusyTag").and_then(|v| v.as_str()),
Some("client-meeting")
);
let back = serde_json::to_value(&b).unwrap();
assert_eq!(back["acmeCorpFreeBusyTag"], "client-meeting");
}
#[test]
fn calendar_rights_preserves_vendor_extras() {
let raw = serde_json::json!({
"mayReadFreeBusy": true,
"mayReadItems": true,
"mayWriteAll": false,
"mayWriteOwn": true,
"mayUpdatePrivate": false,
"mayRSVP": true,
"mayShare": false,
"mayDelete": false,
"acmeCorpMayPublish": false
});
let r: CalendarRights = serde_json::from_value(raw).unwrap();
assert_eq!(
r.extra.get("acmeCorpMayPublish").and_then(|v| v.as_bool()),
Some(false)
);
let back = serde_json::to_value(&r).unwrap();
assert_eq!(back["acmeCorpMayPublish"], false);
}
#[test]
fn calendar_preserves_vendor_extras() {
let raw = serde_json::json!({
"id": "c1",
"name": "Work",
"description": null,
"color": null,
"sortOrder": 0,
"isSubscribed": true,
"isVisible": true,
"isDefault": false,
"includeInAvailability": "all",
"defaultAlertsWithTime": null,
"defaultAlertsWithoutTime": null,
"timeZone": null,
"shareWith": null,
"myRights": {
"mayReadFreeBusy": true, "mayReadItems": true, "mayWriteAll": true,
"mayWriteOwn": true, "mayUpdatePrivate": true, "mayRSVP": true,
"mayShare": true, "mayDelete": true
},
"acmeCorpDepartment": "engineering"
});
let c: Calendar = serde_json::from_value(raw).unwrap();
assert_eq!(
c.extra.get("acmeCorpDepartment").and_then(|v| v.as_str()),
Some("engineering")
);
let back = serde_json::to_value(&c).unwrap();
assert_eq!(back["acmeCorpDepartment"], "engineering");
}
#[test]
fn calendar_event_preserves_vendor_extras() {
let raw = serde_json::json!({
"@type": "Event",
"uid": "event-1",
"title": "Meeting",
"start": "2024-06-01T10:00:00",
"duration": "PT1H",
"acmeCorpMeetingNotes": "https://wiki/n/42"
});
let e: CalendarEvent = serde_json::from_value(raw).unwrap();
assert_eq!(
e.extra.get("acmeCorpMeetingNotes").and_then(|v| v.as_str()),
Some("https://wiki/n/42")
);
let back = serde_json::to_value(&e).unwrap();
assert_eq!(back["acmeCorpMeetingNotes"], "https://wiki/n/42");
}
#[test]
fn person_preserves_vendor_extras() {
let raw = serde_json::json!({
"name": "Alice",
"email": "alice@example.com",
"principalId": null,
"calendarAddress": null,
"acmeCorpEmployeeId": "emp-42"
});
let p: Person = serde_json::from_value(raw).unwrap();
assert_eq!(
p.extra.get("acmeCorpEmployeeId").and_then(|v| v.as_str()),
Some("emp-42")
);
let back = serde_json::to_value(&p).unwrap();
assert_eq!(back["acmeCorpEmployeeId"], "emp-42");
}
#[test]
fn calendar_event_notification_preserves_vendor_extras() {
let raw = serde_json::json!({
"id": "n1",
"created": "2024-06-01T00:00:00Z",
"changedBy": {
"name": "Alice",
"email": null,
"principalId": null,
"calendarAddress": null
},
"type": "created",
"calendarEventId": "e1",
"event": {},
"acmeCorpNotificationChannel": "in-app"
});
let n: CalendarEventNotification = serde_json::from_value(raw).unwrap();
assert_eq!(
n.extra
.get("acmeCorpNotificationChannel")
.and_then(|v| v.as_str()),
Some("in-app")
);
let back = serde_json::to_value(&n).unwrap();
assert_eq!(back["acmeCorpNotificationChannel"], "in-app");
}
#[test]
fn calendar_alert_preserves_vendor_extras() {
let raw = serde_json::json!({
"@type": "CalendarAlert",
"accountId": "a1",
"calendarEventId": "e1",
"uid": "uid-1",
"recurrenceId": null,
"alertId": "alrt-1",
"acmeCorpPushPriority": "high"
});
let a: CalendarAlert = serde_json::from_value(raw).unwrap();
assert_eq!(
a.extra.get("acmeCorpPushPriority").and_then(|v| v.as_str()),
Some("high")
);
let back = serde_json::to_value(&a).unwrap();
assert_eq!(back["acmeCorpPushPriority"], "high");
}
#[test]
fn participant_identity_preserves_vendor_extras() {
let raw = serde_json::json!({
"id": "pi1",
"name": "Alice",
"calendarAddress": "mailto:alice@example.com",
"isDefault": true,
"acmeCorpDeliveryHint": "external"
});
let pi: ParticipantIdentity = serde_json::from_value(raw).unwrap();
assert_eq!(
pi.extra
.get("acmeCorpDeliveryHint")
.and_then(|v| v.as_str()),
Some("external")
);
let back = serde_json::to_value(&pi).unwrap();
assert_eq!(back["acmeCorpDeliveryHint"], "external");
}
#[test]
fn calendar_alert_at_type_defaults_when_absent() {
let raw = serde_json::json!({
"accountId": "acc-1",
"calendarEventId": "ev-42",
"uid": "abc-uid-123",
"recurrenceId": null,
"alertId": "alert-1"
});
let alert: CalendarAlert = serde_json::from_value(raw).unwrap();
assert_eq!(alert.at_type, "CalendarAlert");
let back = serde_json::to_value(&alert).unwrap();
assert_eq!(back["@type"], "CalendarAlert");
}
#[test]
fn calendar_alert_at_type_explicit_value_round_trips_verbatim() {
let raw = serde_json::json!({
"@type": "AcmeCorpAlert",
"accountId": "acc-1",
"calendarEventId": "ev-42",
"uid": "abc-uid-123",
"recurrenceId": null,
"alertId": "alert-1"
});
let alert: CalendarAlert = serde_json::from_value(raw).unwrap();
assert_eq!(alert.at_type, "AcmeCorpAlert");
let back = serde_json::to_value(&alert).unwrap();
assert_eq!(back["@type"], "AcmeCorpAlert");
}
#[test]
fn calendar_event_maps_serialize_in_sorted_key_order() {
let raw = &common::fixture("calendar_event_maps_serialize_in_sorted_key_order.json");
let ev: CalendarEvent = serde_json::from_str(raw).expect("deserialize");
let j = serde_json::to_string(&ev).expect("serialize");
assert!(
j.contains(r#""calendarIds":{"cal-1":true,"cal-2":true}"#),
"calendarIds keys must be sorted: {j}"
);
assert!(
j.contains(r#""localizations":{"es":{"title":"Almuerzo"},"fr":{"title":"Dejeuner"}}"#),
"localizations keys must be sorted: {j}"
);
}
#[test]
fn calendar_rights_may_set_metadata_typed_and_optional() {
let with: serde_json::Value = serde_json::json!({ "mayReadFreeBusy": true, "mayReadItems": true, "mayWriteAll": false, "mayWriteOwn": true, "mayUpdatePrivate": true, "mayRSVP": true, "mayShare": false, "mayDelete": false, "maySetMetadata": true });
let r: CalendarRights = serde_json::from_value(with.clone()).unwrap();
assert_eq!(r.may_set_metadata, Some(true));
assert!(!r.extra.contains_key("maySetMetadata"));
assert_eq!(serde_json::to_value(&r).unwrap(), with);
let denied: serde_json::Value = serde_json::json!({ "mayReadFreeBusy": true, "mayReadItems": true, "mayWriteAll": false, "mayWriteOwn": true, "mayUpdatePrivate": true, "mayRSVP": true, "mayShare": false, "mayDelete": false, "maySetMetadata": false });
let r: CalendarRights = serde_json::from_value(denied.clone()).unwrap();
assert_eq!(r.may_set_metadata, Some(false));
assert_eq!(serde_json::to_value(&r).unwrap(), denied);
let without: serde_json::Value = serde_json::json!({ "mayReadFreeBusy": true, "mayReadItems": true, "mayWriteAll": false, "mayWriteOwn": true, "mayUpdatePrivate": true, "mayRSVP": true, "mayShare": false, "mayDelete": false });
let r: CalendarRights = serde_json::from_value(without.clone()).unwrap();
assert_eq!(r.may_set_metadata, None);
assert_eq!(serde_json::to_value(&r).unwrap(), without);
}
#[test]
fn calendar_event_comparator_sort_properties() {
use jmap_calendars_types::{CalendarEventComparator, CalendarEventComparatorProperty as P};
for (wire, want) in [
("id", P::Id),
("uid", P::Uid),
("start", P::Start),
("created", P::Created),
("updated", P::Updated),
] {
let c: CalendarEventComparator =
serde_json::from_value(serde_json::json!({ "property": wire })).unwrap();
assert_eq!(c.property, want);
assert!(c.is_ascending);
}
for name in ["x-vendor-rank", "recurrenceId"] {
let wire = serde_json::json!({ "property": name, "isAscending": false });
let c: CalendarEventComparator = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(c.property, P::Other(name.to_owned()));
assert_eq!(serde_json::to_value(&c).unwrap(), wire);
}
}