use std::collections::{HashMap, HashSet};
use super::event_change::EventChange;
use crate::calendar::SyncBases;
use crate::event::{EventInstanceId, EventUid, Status};
use crate::{CalendarEvent, DateRange, Event, RemoteEvent};
pub struct CalendarDiff {
outgoing: Vec<EventChange>,
incoming: Vec<EventChange>,
}
impl CalendarDiff {
pub(crate) fn compute(
local_events: Vec<CalendarEvent>,
remote_events: Vec<RemoteEvent>,
sync_bases: &SyncBases,
range: &DateRange,
) -> Self {
let (from, to) = range.query_bounds();
let local_event_ids: HashSet<_> = local_events
.iter()
.map(|e| e.event().event_instance_id())
.collect();
let remote_by_id: HashMap<_, &RemoteEvent> = remote_events
.iter()
.map(|e| (e.event().event_instance_id(), e))
.collect();
let remote_live_master_uids: HashSet<_> = remote_events
.iter()
.map(RemoteEvent::event)
.filter(|event| {
event.recurrence.is_some()
&& event.recurrence_id.is_none()
&& event.status != Status::Cancelled
})
.map(|event| event.uid.clone())
.collect();
let local_masters: HashMap<_, &Event> = local_events
.iter()
.map(CalendarEvent::event)
.filter(|event| event.recurrence.is_some() && event.recurrence_id.is_none())
.map(|event| (event.uid.clone(), event))
.collect();
let mut master_uids: HashSet<_> = local_events
.iter()
.filter(|e| e.event().recurrence.is_some() && e.event().recurrence_id.is_none())
.map(|e| e.event().uid.clone())
.collect();
master_uids.extend(
remote_events
.iter()
.map(RemoteEvent::event)
.filter(|event| {
event.recurrence.is_some()
&& event.recurrence_id.is_none()
&& event.status != Status::Cancelled
&& !local_event_ids.contains(&event.event_instance_id())
&& sync_bases.get(&event.event_instance_id()).is_none()
})
.map(|event| event.uid.clone()),
);
let mut outgoing = Vec::new();
let mut incoming = Vec::new();
for local_event in &local_events {
let event = local_event.event();
let id = event.event_instance_id();
if let Some(remote_event) = remote_by_id.get(&id) {
if event == remote_event.event() {
continue;
}
if event.status == Status::Cancelled
&& remote_event.event().status == Status::Cancelled
{
continue;
}
let mut to_push = event.clone();
if to_push.visibility.is_none() {
to_push.visibility = remote_event.event().visibility;
}
let base = sync_bases.get(&id).and_then(Option::as_deref);
let direction = if &to_push == remote_event.event() {
Some(UpdateDirection::Pull)
} else {
update_direction(local_event, remote_event, base)
};
match direction {
Some(UpdateDirection::Push) => outgoing.push(EventChange::Update {
from: remote_event.event().clone(),
to: to_push,
}),
Some(UpdateDirection::Pull) => incoming.push(EventChange::Update {
from: event.clone(),
to: remote_event.event().clone(),
}),
None => {}
}
continue;
}
if !event.has_occurrence_in_range(from, to) {
continue;
}
if sync_bases.get(&id).is_some()
|| is_orphaned_override(
event,
&remote_live_master_uids,
&local_masters,
sync_bases,
range,
)
{
incoming.push(EventChange::Delete(event.clone()));
} else {
outgoing.push(EventChange::Create(event.clone()));
}
}
for remote_event in &remote_events {
let id = remote_event.event().event_instance_id();
if local_event_ids.contains(&id) {
continue;
}
if remote_event.event().status == Status::Cancelled {
let event = remote_event.event();
if event.recurrence_id.is_some() && master_uids.contains(&event.uid) {
incoming.push(EventChange::Create(event.clone()));
}
continue;
}
if sync_bases.get(&id).is_some() {
outgoing.push(EventChange::Delete(remote_event.event().clone()));
} else {
incoming.push(EventChange::Create(remote_event.event().clone()));
}
}
CalendarDiff { outgoing, incoming }
}
pub fn incoming(&self) -> &[EventChange] {
&self.incoming
}
pub fn outgoing(&self) -> &[EventChange] {
&self.outgoing
}
pub fn is_empty(&self) -> bool {
self.outgoing.is_empty() && self.incoming.is_empty()
}
pub fn discard_outgoing(&mut self) {
self.outgoing.clear();
}
}
fn is_orphaned_override(
event: &Event,
remote_live_master_uids: &HashSet<EventUid>,
local_masters: &HashMap<EventUid, &Event>,
sync_bases: &SyncBases,
range: &DateRange,
) -> bool {
if event.recurrence_id.is_none() || remote_live_master_uids.contains(&event.uid) {
return false;
}
let master_id = EventInstanceId::new(event.uid.clone(), None);
if sync_bases.get(&master_id).is_none() {
return false;
}
let Some(master) = local_masters.get(&event.uid) else {
return true;
};
let (from, to) = range.query_bounds();
master.has_occurrence_in_range(from, to)
}
#[cfg(test)]
impl CalendarDiff {
pub(crate) fn from_changes(outgoing: Vec<EventChange>, incoming: Vec<EventChange>) -> Self {
Self { outgoing, incoming }
}
}
enum UpdateDirection {
Push,
Pull,
}
fn update_direction(
local: &CalendarEvent,
remote: &RemoteEvent,
base: Option<&crate::Event>,
) -> Option<UpdateDirection> {
if let Some(base) = base {
let local_has_changes = local.event() != base;
let remote_has_changes = remote.event() != base;
match (local_has_changes, remote_has_changes) {
(false, false) => return None,
(true, false) => return Some(UpdateDirection::Push),
(false, true) => return Some(UpdateDirection::Pull),
(true, true) => {}
}
}
if local_is_newer(local, remote) {
Some(UpdateDirection::Push)
} else {
Some(UpdateDirection::Pull)
}
}
fn local_is_newer(local: &CalendarEvent, remote: &RemoteEvent) -> bool {
if remote.modified_at().is_none() && local.event().sequence != remote.event().sequence {
return local.event().sequence > remote.event().sequence;
}
match (local.modified_at(), remote.modified_at()) {
(Some(l), Some(r)) => l > r,
(Some(_), None) => true,
(None, Some(_)) => false,
(None, None) => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Event;
use crate::event::{EventTime, Recurrence, RecurrenceId};
use crate::test_utils::{test_calendar, test_calendar_event, test_event};
use chrono::{TimeZone, Utc};
use pretty_assertions::assert_eq;
fn recurring_series() -> (Event, Event) {
let mut master = test_event();
master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
let mut override_event = master.occurrence_at(master.start.clone());
override_event.summary = Some("Overridden occurrence".to_string());
(master, override_event)
}
#[test]
fn new_local_event_becomes_outgoing_create() {
let (_tmp, calendar_event) = test_calendar_event();
let new_event = calendar_event.event().clone();
let local_events = vec![calendar_event];
let remote_events = vec![];
let synced_ids = SyncBases::new();
let diff = CalendarDiff::compute(
local_events,
remote_events,
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![EventChange::Create(new_event)]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn new_remote_event_becomes_incoming_create() {
let new_event = test_event();
let local_events = vec![];
let remote_events = vec![RemoteEvent::new(new_event.clone())];
let synced_ids = SyncBases::new();
let diff = CalendarDiff::compute(
local_events,
remote_events,
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![EventChange::Create(new_event)]);
}
#[test]
fn orphaned_override_is_deleted_after_master_already_deleted() {
for full_base in [true, false] {
let (_tmp, calendar) = test_calendar();
let (master, override_event) = recurring_series();
let calendar_event = calendar.create_event(override_event.clone()).unwrap();
let mut sync_bases = SyncBases::new();
if full_base {
sync_bases.insert_event_base(master.event_instance_id(), master);
} else {
sync_bases.insert_known_event_id(master.event_instance_id());
}
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![EventChange::Delete(override_event)]);
}
}
#[test]
fn cancelled_remote_master_is_not_considered_live() {
let (_tmp, calendar) = test_calendar();
let (master, override_event) = recurring_series();
let local_master = calendar.create_event(master.clone()).unwrap();
let local_override = calendar.create_event(override_event.clone()).unwrap();
let mut cancelled_master = master.clone();
cancelled_master.status = Status::Cancelled;
let mut sync_bases = SyncBases::new();
sync_bases.insert_event_base(master.event_instance_id(), master.clone());
let diff = CalendarDiff::compute(
vec![local_master, local_override],
vec![RemoteEvent::new(cancelled_master.clone())],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![
EventChange::Update {
from: master,
to: cancelled_master,
},
EventChange::Delete(override_event),
]
);
}
#[test]
fn new_local_series_still_pushes_master_and_override() {
let (_tmp, calendar) = test_calendar();
let (master, override_event) = recurring_series();
let local_master = calendar.create_event(master.clone()).unwrap();
let local_override = calendar.create_event(override_event.clone()).unwrap();
let diff = CalendarDiff::compute(
vec![local_master, local_override],
vec![],
&SyncBases::new(),
&DateRange::default(),
);
assert_eq!(
diff.outgoing,
vec![
EventChange::Create(master),
EventChange::Create(override_event),
]
);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn fresh_override_of_live_remote_series_still_pushes() {
let (_tmp, calendar) = test_calendar();
let (master, override_event) = recurring_series();
let local_master = calendar.create_event(master.clone()).unwrap();
let local_override = calendar.create_event(override_event.clone()).unwrap();
let mut sync_bases = SyncBases::new();
sync_bases.insert_event_base(master.event_instance_id(), master.clone());
let diff = CalendarDiff::compute(
vec![local_master, local_override],
vec![RemoteEvent::new(master)],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![EventChange::Create(override_event)]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn orphan_rule_requires_synced_master_evidence() {
let (_tmp, calendar) = test_calendar();
let (_master, override_event) = recurring_series();
let local_override = calendar.create_event(override_event.clone()).unwrap();
let diff = CalendarDiff::compute(
vec![local_override],
vec![],
&SyncBases::new(),
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![EventChange::Create(override_event)]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn orphan_rule_is_conservative_when_local_master_is_out_of_range() {
let (_tmp, calendar) = test_calendar();
let (mut master, mut override_event) = recurring_series();
master.recurrence = Some(Recurrence::new("FREQ=DAILY;COUNT=2"));
override_event.start = EventTime::DateTimeFloating(
chrono::NaiveDate::from_ymd_opt(2027, 1, 1)
.unwrap()
.and_hms_opt(12, 0, 0)
.unwrap(),
);
let local_master = calendar.create_event(master.clone()).unwrap();
let local_override = calendar.create_event(override_event.clone()).unwrap();
let mut sync_bases = SyncBases::new();
sync_bases.insert_event_base(master.event_instance_id(), master);
let range = DateRange {
from: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
to: Some(Utc.with_ymd_and_hms(2027, 1, 2, 0, 0, 0).unwrap()),
};
let diff = CalendarDiff::compute(
vec![local_master, local_override],
vec![],
&sync_bases,
&range,
);
assert_eq!(diff.outgoing, vec![EventChange::Create(override_event)]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn deleted_local_event_becomes_outgoing_delete() {
let remote_event = test_event();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(remote_event.event_instance_id());
let local_events = vec![];
let remote_events = vec![RemoteEvent::new(remote_event.clone())];
let diff = CalendarDiff::compute(
local_events,
remote_events,
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![EventChange::Delete(remote_event)]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn invitee_seeing_cancellation_gets_status_flip_update() {
let (_tmp, calendar) = test_calendar();
let event = test_event();
let calendar_event = calendar.create_event(event.clone()).unwrap();
let mut cancelled = event.clone();
cancelled.status = Status::Cancelled;
cancelled.last_modified = Some(Utc.with_ymd_and_hms(3000, 1, 1, 0, 0, 0).unwrap());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(cancelled.clone())],
&SyncBases::new(),
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![EventChange::Update {
from: event,
to: cancelled,
}]
);
}
#[test]
fn missing_local_cancelled_remote_is_skipped() {
let mut cancelled = test_event();
cancelled.status = Status::Cancelled;
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(cancelled.event_instance_id());
let diff = CalendarDiff::compute(
vec![],
vec![RemoteEvent::new(cancelled)],
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn cancelled_instance_of_local_master_becomes_incoming_create() {
let (_tmp, calendar) = test_calendar();
let mut master = test_event();
master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
let calendar_event = calendar.create_event(master.clone()).unwrap();
let mut cancelled_instance = master.clone();
cancelled_instance.recurrence = None;
cancelled_instance.recurrence_id =
Some(RecurrenceId::from_event_time(master.start.clone()));
cancelled_instance.status = Status::Cancelled;
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![
RemoteEvent::new(master),
RemoteEvent::new(cancelled_instance.clone()),
],
&SyncBases::new(),
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![EventChange::Create(cancelled_instance)]);
}
#[test]
fn first_sync_pulls_master_and_cancelled_instance() {
let mut master = test_event();
master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
let mut cancelled_instance = master.clone();
cancelled_instance.recurrence = None;
cancelled_instance.recurrence_id =
Some(RecurrenceId::from_event_time(master.start.clone()));
cancelled_instance.status = Status::Cancelled;
let diff = CalendarDiff::compute(
vec![],
vec![
RemoteEvent::new(master.clone()),
RemoteEvent::new(cancelled_instance.clone()),
],
&SyncBases::new(),
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![
EventChange::Create(master),
EventChange::Create(cancelled_instance),
]
);
}
#[test]
fn cancelled_instance_is_skipped_when_local_master_was_deleted() {
let mut master = test_event();
master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
let mut cancelled_instance = master.clone();
cancelled_instance.recurrence = None;
cancelled_instance.recurrence_id =
Some(RecurrenceId::from_event_time(master.start.clone()));
cancelled_instance.status = Status::Cancelled;
let mut sync_bases = SyncBases::new();
sync_bases.insert_known_event_id(master.event_instance_id());
let diff = CalendarDiff::compute(
vec![],
vec![
RemoteEvent::new(master.clone()),
RemoteEvent::new(cancelled_instance),
],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![EventChange::Delete(master)]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn cancelled_instance_is_skipped_when_remote_master_is_cancelled() {
let mut master = test_event();
master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
master.status = Status::Cancelled;
let mut cancelled_instance = master.clone();
cancelled_instance.recurrence = None;
cancelled_instance.recurrence_id =
Some(RecurrenceId::from_event_time(master.start.clone()));
let diff = CalendarDiff::compute(
vec![],
vec![
RemoteEvent::new(master),
RemoteEvent::new(cancelled_instance),
],
&SyncBases::new(),
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn both_cancelled_with_field_drift_is_skipped() {
let (_tmp, calendar) = test_calendar();
let mut local = test_event();
local.status = Status::Cancelled;
let calendar_event = calendar.create_event(local.clone()).unwrap();
let mut remote = local.clone();
remote.summary = Some("Now with extra data".into());
remote.location = Some("Somewhere".into());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote)],
&SyncBases::new(),
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn expired_recurring_event_not_flagged_as_delete() {
let (_tmp, calendar) = test_calendar();
let mut event = Event::new(
"Old recurring",
EventTime::Date(chrono::NaiveDate::from_ymd_opt(1968, 5, 8).unwrap()),
);
event.recurrence = Some(crate::event::Recurrence::new(
"FREQ=YEARLY;UNTIL=20080507T120000Z",
));
let calendar_event = calendar.create_event(event.clone()).unwrap();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(event.event_instance_id());
let range = DateRange {
from: Some(Utc.with_ymd_and_hms(2025, 5, 14, 0, 0, 0).unwrap()),
to: Some(Utc.with_ymd_and_hms(2027, 5, 14, 0, 0, 0).unwrap()),
};
let diff = CalendarDiff::compute(vec![calendar_event], vec![], &synced_ids, &range);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn deleted_remote_event_becomes_incoming_delete() {
let (_tmp, calendar_event) = test_calendar_event();
let local_event = calendar_event.event().clone();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(local_event.event_instance_id());
let local_events = vec![calendar_event];
let remote_events = vec![];
let diff = CalendarDiff::compute(
local_events,
remote_events,
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![EventChange::Delete(local_event)]);
}
#[test]
fn event_base_marks_event_as_previously_synced() {
let (_tmp, calendar_event) = test_calendar_event();
let local_event = calendar_event.event().clone();
let id = local_event.event_instance_id();
let mut sync_bases = SyncBases::new();
sync_bases.insert_event_base(id, local_event.clone());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![EventChange::Delete(local_event)]);
}
#[test]
fn updated_local_event_becomes_outgoing_update() {
let (_tmp, calendar) = test_calendar();
let remote_event = test_event();
let mut local_event = remote_event.clone();
local_event.summary = Some("Updated Test Event".to_string());
let calendar_event = calendar.create_event(local_event.clone()).unwrap();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(remote_event.event_instance_id());
let local_events = vec![calendar_event];
let remote_events = vec![RemoteEvent::new(remote_event.clone())];
let diff = CalendarDiff::compute(
local_events,
remote_events,
&synced_ids,
&DateRange::default(),
);
assert_eq!(
diff.outgoing,
vec![EventChange::Update {
from: remote_event,
to: local_event,
}]
);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn higher_remote_sequence_breaks_conflict_without_modified_time() {
let (_tmp, calendar) = test_calendar();
let mut base = test_event();
base.sequence = 1;
let mut local = base.clone();
local.summary = Some("Edited locally".to_string());
local.sequence = 2;
let calendar_event = calendar.create_event(local.clone()).unwrap();
let mut remote = base.clone();
remote.summary = Some("Edited remotely".to_string());
remote.sequence = 3;
remote.last_modified = None;
let mut sync_bases = SyncBases::new();
sync_bases.insert_event_base(base.event_instance_id(), base);
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote.clone())],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![EventChange::Update {
from: local,
to: remote,
}]
);
}
#[test]
fn remote_update_without_modified_time_is_pulled_when_local_matches_base() {
let (_tmp, calendar) = test_calendar();
let base = test_event();
let calendar_event = calendar.create_event(base.clone()).unwrap();
assert!(calendar_event.modified_at().is_some());
let mut remote = base.clone();
remote.summary = Some("Edited remotely".to_string());
remote.last_modified = None;
let mut sync_bases = SyncBases::new();
sync_bases.insert_event_base(base.event_instance_id(), base.clone());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote.clone())],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![EventChange::Update {
from: base,
to: remote,
}]
);
}
#[test]
fn remote_update_is_pulled_when_unchanged_local_file_has_newer_mtime() {
let (_tmp, calendar) = test_calendar();
let mut local = test_event();
local.last_modified = Some(Utc.with_ymd_and_hms(2026, 3, 5, 8, 45, 16).unwrap());
let calendar_event = calendar.create_event(local.clone()).unwrap();
let mut base = local.clone();
base.last_modified = None;
let mut remote = base.clone();
remote.summary = Some("Edited remotely".to_string());
remote.last_modified = Some(Utc.with_ymd_and_hms(2025, 6, 9, 10, 42, 20).unwrap());
assert!(calendar_event.modified_at() > remote.last_modified);
let mut sync_bases = SyncBases::new();
sync_bases.insert_event_base(base.event_instance_id(), base.clone());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote.clone())],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![EventChange::Update {
from: local,
to: remote,
}]
);
}
#[test]
fn local_update_is_pushed_when_unchanged_remote_has_newer_modified_time() {
let (_tmp, calendar) = test_calendar();
let base = test_event();
let mut local = base.clone();
local.summary = Some("Edited locally".to_string());
local.last_modified = Some(Utc.with_ymd_and_hms(2025, 6, 9, 10, 42, 20).unwrap());
let calendar_event = calendar.create_event(local.clone()).unwrap();
let mut remote = base.clone();
remote.last_modified = Some(Utc.with_ymd_and_hms(2026, 3, 5, 8, 45, 16).unwrap());
assert!(calendar_event.modified_at() < remote.last_modified);
let mut sync_bases = SyncBases::new();
sync_bases.insert_event_base(base.event_instance_id(), base);
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote.clone())],
&sync_bases,
&DateRange::default(),
);
assert_eq!(diff.incoming, vec![]);
assert_eq!(
diff.outgoing,
vec![EventChange::Update {
from: remote,
to: local,
}]
);
}
#[test]
fn sync_metadata_only_differences_produce_no_diff() {
let (_tmp, calendar) = test_calendar();
let local_event = test_event();
let mut remote_event = local_event.clone();
remote_event.last_modified = Some(Utc::now() + chrono::Duration::days(1));
remote_event.sequence += 1;
let calendar_event = calendar.create_event(local_event.clone()).unwrap();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(local_event.event_instance_id());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote_event)],
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn equal_timestamps_with_field_drift_resolve_to_pull() {
let (_tmp, calendar) = test_calendar();
let when = Utc.with_ymd_and_hms(2026, 5, 15, 13, 12, 52).unwrap();
let mut local_event = test_event();
local_event.last_modified = Some(when);
let calendar_event = calendar.create_event(local_event.clone()).unwrap();
let mut remote_event = local_event.clone();
remote_event.visibility = Some(crate::event::Visibility::Private);
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(local_event.event_instance_id());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote_event.clone())],
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![EventChange::Update {
from: local_event,
to: remote_event,
}]
);
}
#[test]
fn unspecified_local_visibility_resolves_to_pull_even_when_local_mtime_is_newer() {
let (_tmp, calendar) = test_calendar();
let mut local_event = test_event();
local_event.last_modified = Some(Utc.with_ymd_and_hms(2026, 3, 5, 8, 45, 16).unwrap());
assert_eq!(local_event.visibility, None);
let calendar_event = calendar.create_event(local_event.clone()).unwrap();
let mut remote_event = local_event.clone();
remote_event.visibility = Some(crate::event::Visibility::Private);
remote_event.last_modified = Some(Utc.with_ymd_and_hms(2025, 6, 9, 10, 42, 20).unwrap());
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(local_event.event_instance_id());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote_event.clone())],
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![EventChange::Update {
from: local_event,
to: remote_event,
}]
);
}
#[test]
fn outgoing_update_keeps_remote_visibility_when_local_is_unspecified() {
let (_tmp, calendar) = test_calendar();
let mut local_event = test_event();
local_event.summary = Some("Edited locally".to_string());
local_event.last_modified = Some(Utc.with_ymd_and_hms(2026, 3, 5, 8, 45, 16).unwrap());
let calendar_event = calendar.create_event(local_event.clone()).unwrap();
let mut remote_event = local_event.clone();
remote_event.summary = Some("Test Event".to_string());
remote_event.visibility = Some(crate::event::Visibility::Private);
remote_event.last_modified = Some(Utc.with_ymd_and_hms(2025, 6, 9, 10, 42, 20).unwrap());
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(local_event.event_instance_id());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote_event)],
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.incoming, vec![]);
match diff.outgoing.as_slice() {
[EventChange::Update { to, .. }] => {
assert_eq!(to.summary.as_deref(), Some("Edited locally"));
assert_eq!(to.visibility, Some(crate::event::Visibility::Private));
}
other => panic!("expected one outgoing Update, got {other:?}"),
}
}
#[test]
fn x_property_change_surfaces_as_diff() {
let (_tmp, calendar) = test_calendar();
let mut local_event = test_event();
local_event
.x_properties
.push(crate::event::XProperty::new("X-GOOGLE-COLOR-ID", "5"));
let mut remote_event = local_event.clone();
remote_event.x_properties = vec![crate::event::XProperty::new("X-GOOGLE-COLOR-ID", "9")];
remote_event.last_modified = Some(Utc::now() + chrono::Duration::days(1));
let calendar_event = calendar.create_event(local_event.clone()).unwrap();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(local_event.event_instance_id());
let diff = CalendarDiff::compute(
vec![calendar_event],
vec![RemoteEvent::new(remote_event.clone())],
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![EventChange::Update {
from: local_event,
to: remote_event,
}]
);
}
#[test]
fn x_properties_compare_equal_regardless_of_order() {
let mut event_a = test_event();
event_a.x_properties = vec![
crate::event::XProperty::new("X-GOOGLE-EVENT-ID", "abc"),
crate::event::XProperty::new("X-GOOGLE-COLOR-ID", "5"),
];
let mut event_b = event_a.clone();
event_b.x_properties = vec![
crate::event::XProperty::new("X-GOOGLE-COLOR-ID", "5"),
crate::event::XProperty::new("X-GOOGLE-EVENT-ID", "abc"),
];
assert_eq!(event_a, event_b);
}
#[test]
fn updated_remote_event_becomes_incoming_update() {
let (_tmp, calendar) = test_calendar();
let local_event = test_event();
let mut remote_event = local_event.clone();
remote_event.summary = Some("Updated Test Event".to_string());
remote_event.last_modified = Some(Utc::now() + chrono::Duration::days(1));
let calendar_event = calendar.create_event(local_event.clone()).unwrap();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(local_event.event_instance_id());
let local_events = vec![calendar_event];
let remote_events = vec![RemoteEvent::new(remote_event.clone())];
let diff = CalendarDiff::compute(
local_events,
remote_events,
&synced_ids,
&DateRange::default(),
);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(
diff.incoming,
vec![EventChange::Update {
from: local_event,
to: remote_event,
}]
);
}
#[test]
fn local_event_outside_window_is_not_flagged_for_delete() {
let (_tmp, calendar) = test_calendar();
let mut old_event = test_event();
old_event.start =
EventTime::DateTimeUtc(Utc.with_ymd_and_hms(2020, 1, 1, 9, 0, 0).unwrap());
old_event.end = Some(EventTime::DateTimeUtc(
Utc.with_ymd_and_hms(2020, 1, 1, 10, 0, 0).unwrap(),
));
let calendar_event = calendar.create_event(old_event.clone()).unwrap();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(old_event.event_instance_id());
let range = DateRange {
from: Some(Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()),
to: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
};
let diff = CalendarDiff::compute(vec![calendar_event], vec![], &synced_ids, &range);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![]);
}
#[test]
fn local_event_inside_window_is_flagged_for_delete_when_remote_drops_it() {
let (_tmp, calendar) = test_calendar();
let mut event = test_event();
event.start = EventTime::DateTimeUtc(Utc.with_ymd_and_hms(2026, 6, 15, 9, 0, 0).unwrap());
event.end = Some(EventTime::DateTimeUtc(
Utc.with_ymd_and_hms(2026, 6, 15, 10, 0, 0).unwrap(),
));
let calendar_event = calendar.create_event(event.clone()).unwrap();
let mut synced_ids = SyncBases::new();
synced_ids.insert_known_event_id(event.event_instance_id());
let range = DateRange {
from: Some(Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()),
to: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
};
let diff = CalendarDiff::compute(vec![calendar_event], vec![], &synced_ids, &range);
assert_eq!(diff.outgoing, vec![]);
assert_eq!(diff.incoming, vec![EventChange::Delete(event)]);
}
#[test]
fn one_sided_window_leaves_out_of_range_local_events_untouched() {
let boundary = Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap();
for (year, range) in [
(
2028,
DateRange {
from: None,
to: Some(boundary),
},
),
(
2026,
DateRange {
from: Some(boundary),
to: None,
},
),
] {
for synced in [false, true] {
for recurring in [false, true] {
let (_tmp, calendar) = test_calendar();
let mut event = test_event();
event.start = EventTime::DateTimeUtc(
Utc.with_ymd_and_hms(year, 2, 25, 15, 45, 0).unwrap(),
);
event.end = None;
if recurring {
event.recurrence = Some(Recurrence::new("FREQ=DAILY;COUNT=2"));
}
let local = calendar.create_event(event.clone()).unwrap();
let mut bases = SyncBases::new();
if synced {
bases.insert_known_event_id(event.event_instance_id());
}
let diff = CalendarDiff::compute(vec![local], vec![], &bases, &range);
assert!(
diff.is_empty(),
"year={year}, synced={synced}, recurring={recurring}"
);
}
}
}
}
#[test]
fn one_sided_window_still_detects_remote_deletions_in_range() {
for range in [
DateRange {
from: None,
to: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
},
DateRange {
from: Some(Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()),
to: None,
},
] {
let (_tmp, local) = test_calendar_event();
let event = local.event().clone();
let mut bases = SyncBases::new();
bases.insert_known_event_id(event.event_instance_id());
let diff = CalendarDiff::compute(vec![local], vec![], &bases, &range);
assert_eq!(diff.incoming, vec![EventChange::Delete(event)]);
assert!(diff.outgoing.is_empty());
}
}
#[test]
fn one_sided_window_does_not_treat_out_of_range_master_as_deleted() {
for (year, range) in [
(
2025,
DateRange {
from: None,
to: Some(Utc.with_ymd_and_hms(2025, 12, 31, 0, 0, 0).unwrap()),
},
),
(
2027,
DateRange {
from: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
to: None,
},
),
] {
let (_tmp, calendar) = test_calendar();
let (mut master, mut override_event) = recurring_series();
master.recurrence = Some(Recurrence::new("FREQ=DAILY;COUNT=2"));
override_event.start =
EventTime::DateTimeUtc(Utc.with_ymd_and_hms(year, 6, 1, 12, 0, 0).unwrap());
override_event.end = None;
let local_master = calendar.create_event(master.clone()).unwrap();
let local_override = calendar.create_event(override_event.clone()).unwrap();
let mut bases = SyncBases::new();
bases.insert_event_base(master.event_instance_id(), master);
let diff =
CalendarDiff::compute(vec![local_master, local_override], vec![], &bases, &range);
assert!(diff.incoming.is_empty());
assert_eq!(diff.outgoing, vec![EventChange::Create(override_event)]);
}
}
}