1use std::collections::{HashMap, HashSet};
2
3use super::event_change::EventChange;
4use crate::calendar::SyncBases;
5use crate::event::{EventInstanceId, EventUid, Status};
6use crate::{CalendarEvent, DateRange, Event, RemoteEvent};
7
8pub struct CalendarDiff {
9 outgoing: Vec<EventChange>,
10 incoming: Vec<EventChange>,
11}
12
13impl CalendarDiff {
14 pub(crate) fn compute(
15 local_events: Vec<CalendarEvent>,
16 remote_events: Vec<RemoteEvent>,
17 sync_bases: &SyncBases,
18 range: &DateRange,
19 ) -> Self {
20 let (from, to) = range.query_bounds();
21 let local_event_ids: HashSet<_> = local_events
22 .iter()
23 .map(|e| e.event().event_instance_id())
24 .collect();
25
26 let remote_by_id: HashMap<_, &RemoteEvent> = remote_events
27 .iter()
28 .map(|e| (e.event().event_instance_id(), e))
29 .collect();
30
31 let remote_live_master_uids: HashSet<_> = remote_events
32 .iter()
33 .map(RemoteEvent::event)
34 .filter(|event| {
35 event.recurrence.is_some()
36 && event.recurrence_id.is_none()
37 && event.status != Status::Cancelled
38 })
39 .map(|event| event.uid.clone())
40 .collect();
41
42 let local_masters: HashMap<_, &Event> = local_events
43 .iter()
44 .map(CalendarEvent::event)
45 .filter(|event| event.recurrence.is_some() && event.recurrence_id.is_none())
46 .map(|event| (event.uid.clone(), event))
47 .collect();
48
49 let mut master_uids: HashSet<_> = local_events
50 .iter()
51 .filter(|e| e.event().recurrence.is_some() && e.event().recurrence_id.is_none())
52 .map(|e| e.event().uid.clone())
53 .collect();
54
55 master_uids.extend(
56 remote_events
57 .iter()
58 .map(RemoteEvent::event)
59 .filter(|event| {
60 event.recurrence.is_some()
61 && event.recurrence_id.is_none()
62 && event.status != Status::Cancelled
63 && !local_event_ids.contains(&event.event_instance_id())
64 && sync_bases.get(&event.event_instance_id()).is_none()
65 })
66 .map(|event| event.uid.clone()),
67 );
68
69 let mut outgoing = Vec::new();
70 let mut incoming = Vec::new();
71
72 for local_event in &local_events {
73 let event = local_event.event();
74 let id = event.event_instance_id();
75
76 if let Some(remote_event) = remote_by_id.get(&id) {
78 if event == remote_event.event() {
79 continue;
80 }
81
82 if event.status == Status::Cancelled
85 && remote_event.event().status == Status::Cancelled
86 {
87 continue;
88 }
89
90 let mut to_push = event.clone();
92 if to_push.visibility.is_none() {
93 to_push.visibility = remote_event.event().visibility;
94 }
95
96 let base = sync_bases.get(&id).and_then(Option::as_deref);
97 let direction = if &to_push == remote_event.event() {
98 Some(UpdateDirection::Pull)
99 } else {
100 update_direction(local_event, remote_event, base)
101 };
102
103 match direction {
104 Some(UpdateDirection::Push) => outgoing.push(EventChange::Update {
105 from: remote_event.event().clone(),
106 to: to_push,
107 }),
108 Some(UpdateDirection::Pull) => incoming.push(EventChange::Update {
109 from: event.clone(),
110 to: remote_event.event().clone(),
111 }),
112 None => {}
113 }
114 continue;
115 }
116
117 if !event.has_occurrence_in_range(from, to) {
120 continue;
121 }
122
123 if sync_bases.get(&id).is_some()
124 || is_orphaned_override(
125 event,
126 &remote_live_master_uids,
127 &local_masters,
128 sync_bases,
129 range,
130 )
131 {
132 incoming.push(EventChange::Delete(event.clone()));
133 } else {
134 outgoing.push(EventChange::Create(event.clone()));
135 }
136 }
137
138 for remote_event in &remote_events {
139 let id = remote_event.event().event_instance_id();
140
141 if local_event_ids.contains(&id) {
143 continue;
144 }
145
146 if remote_event.event().status == Status::Cancelled {
149 let event = remote_event.event();
150
151 if event.recurrence_id.is_some() && master_uids.contains(&event.uid) {
152 incoming.push(EventChange::Create(event.clone()));
153 }
154
155 continue;
156 }
157
158 if sync_bases.get(&id).is_some() {
159 outgoing.push(EventChange::Delete(remote_event.event().clone()));
161 } else {
162 incoming.push(EventChange::Create(remote_event.event().clone()));
164 }
165 }
166
167 CalendarDiff { outgoing, incoming }
168 }
169
170 pub fn incoming(&self) -> &[EventChange] {
171 &self.incoming
172 }
173
174 pub fn outgoing(&self) -> &[EventChange] {
175 &self.outgoing
176 }
177
178 pub fn is_empty(&self) -> bool {
179 self.outgoing.is_empty() && self.incoming.is_empty()
180 }
181
182 pub fn discard_outgoing(&mut self) {
185 self.outgoing.clear();
186 }
187}
188
189fn is_orphaned_override(
190 event: &Event,
191 remote_live_master_uids: &HashSet<EventUid>,
192 local_masters: &HashMap<EventUid, &Event>,
193 sync_bases: &SyncBases,
194 range: &DateRange,
195) -> bool {
196 if event.recurrence_id.is_none() || remote_live_master_uids.contains(&event.uid) {
197 return false;
198 }
199
200 let master_id = EventInstanceId::new(event.uid.clone(), None);
201 if sync_bases.get(&master_id).is_none() {
202 return false;
203 }
204
205 let Some(master) = local_masters.get(&event.uid) else {
206 return true;
207 };
208
209 let (from, to) = range.query_bounds();
210 master.has_occurrence_in_range(from, to)
211}
212
213#[cfg(test)]
214impl CalendarDiff {
215 pub(crate) fn from_changes(outgoing: Vec<EventChange>, incoming: Vec<EventChange>) -> Self {
216 Self { outgoing, incoming }
217 }
218}
219
220enum UpdateDirection {
221 Push,
222 Pull,
223}
224
225fn update_direction(
226 local: &CalendarEvent,
227 remote: &RemoteEvent,
228 base: Option<&crate::Event>,
229) -> Option<UpdateDirection> {
230 if let Some(base) = base {
232 let local_has_changes = local.event() != base;
233 let remote_has_changes = remote.event() != base;
234
235 match (local_has_changes, remote_has_changes) {
236 (false, false) => return None,
237 (true, false) => return Some(UpdateDirection::Push),
238 (false, true) => return Some(UpdateDirection::Pull),
239 (true, true) => {}
240 }
241 }
242
243 if local_is_newer(local, remote) {
246 Some(UpdateDirection::Push)
247 } else {
248 Some(UpdateDirection::Pull)
249 }
250}
251
252fn local_is_newer(local: &CalendarEvent, remote: &RemoteEvent) -> bool {
253 if remote.modified_at().is_none() && local.event().sequence != remote.event().sequence {
254 return local.event().sequence > remote.event().sequence;
255 }
256
257 match (local.modified_at(), remote.modified_at()) {
258 (Some(l), Some(r)) => l > r,
259 (Some(_), None) => true,
260 (None, Some(_)) => false,
261 (None, None) => true,
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::Event;
269 use crate::event::{EventTime, Recurrence, RecurrenceId};
270 use crate::test_utils::{test_calendar, test_calendar_event, test_event};
271 use chrono::{TimeZone, Utc};
272 use pretty_assertions::assert_eq;
273
274 fn recurring_series() -> (Event, Event) {
275 let mut master = test_event();
276 master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
277
278 let mut override_event = master.occurrence_at(master.start.clone());
279 override_event.summary = Some("Overridden occurrence".to_string());
280
281 (master, override_event)
282 }
283
284 #[test]
285 fn new_local_event_becomes_outgoing_create() {
286 let (_tmp, calendar_event) = test_calendar_event();
287 let new_event = calendar_event.event().clone();
288
289 let local_events = vec![calendar_event];
290 let remote_events = vec![];
291 let synced_ids = SyncBases::new();
292
293 let diff = CalendarDiff::compute(
294 local_events,
295 remote_events,
296 &synced_ids,
297 &DateRange::default(),
298 );
299
300 assert_eq!(diff.outgoing, vec![EventChange::Create(new_event)]);
301 assert_eq!(diff.incoming, vec![]);
302 }
303
304 #[test]
305 fn new_remote_event_becomes_incoming_create() {
306 let new_event = test_event();
307
308 let local_events = vec![];
309 let remote_events = vec![RemoteEvent::new(new_event.clone())];
310 let synced_ids = SyncBases::new();
311
312 let diff = CalendarDiff::compute(
313 local_events,
314 remote_events,
315 &synced_ids,
316 &DateRange::default(),
317 );
318
319 assert_eq!(diff.outgoing, vec![]);
320 assert_eq!(diff.incoming, vec![EventChange::Create(new_event)]);
321 }
322
323 #[test]
324 fn orphaned_override_is_deleted_after_master_already_deleted() {
325 for full_base in [true, false] {
326 let (_tmp, calendar) = test_calendar();
327 let (master, override_event) = recurring_series();
328 let calendar_event = calendar.create_event(override_event.clone()).unwrap();
329
330 let mut sync_bases = SyncBases::new();
331 if full_base {
332 sync_bases.insert_event_base(master.event_instance_id(), master);
333 } else {
334 sync_bases.insert_known_event_id(master.event_instance_id());
335 }
336
337 let diff = CalendarDiff::compute(
338 vec![calendar_event],
339 vec![],
340 &sync_bases,
341 &DateRange::default(),
342 );
343
344 assert_eq!(diff.outgoing, vec![]);
345 assert_eq!(diff.incoming, vec![EventChange::Delete(override_event)]);
346 }
347 }
348
349 #[test]
350 fn cancelled_remote_master_is_not_considered_live() {
351 let (_tmp, calendar) = test_calendar();
352 let (master, override_event) = recurring_series();
353 let local_master = calendar.create_event(master.clone()).unwrap();
354 let local_override = calendar.create_event(override_event.clone()).unwrap();
355
356 let mut cancelled_master = master.clone();
357 cancelled_master.status = Status::Cancelled;
358
359 let mut sync_bases = SyncBases::new();
360 sync_bases.insert_event_base(master.event_instance_id(), master.clone());
361
362 let diff = CalendarDiff::compute(
363 vec![local_master, local_override],
364 vec![RemoteEvent::new(cancelled_master.clone())],
365 &sync_bases,
366 &DateRange::default(),
367 );
368
369 assert_eq!(diff.outgoing, vec![]);
370 assert_eq!(
371 diff.incoming,
372 vec![
373 EventChange::Update {
374 from: master,
375 to: cancelled_master,
376 },
377 EventChange::Delete(override_event),
378 ]
379 );
380 }
381
382 #[test]
383 fn new_local_series_still_pushes_master_and_override() {
384 let (_tmp, calendar) = test_calendar();
385 let (master, override_event) = recurring_series();
386 let local_master = calendar.create_event(master.clone()).unwrap();
387 let local_override = calendar.create_event(override_event.clone()).unwrap();
388
389 let diff = CalendarDiff::compute(
390 vec![local_master, local_override],
391 vec![],
392 &SyncBases::new(),
393 &DateRange::default(),
394 );
395
396 assert_eq!(
397 diff.outgoing,
398 vec![
399 EventChange::Create(master),
400 EventChange::Create(override_event),
401 ]
402 );
403 assert_eq!(diff.incoming, vec![]);
404 }
405
406 #[test]
407 fn fresh_override_of_live_remote_series_still_pushes() {
408 let (_tmp, calendar) = test_calendar();
409 let (master, override_event) = recurring_series();
410 let local_master = calendar.create_event(master.clone()).unwrap();
411 let local_override = calendar.create_event(override_event.clone()).unwrap();
412
413 let mut sync_bases = SyncBases::new();
414 sync_bases.insert_event_base(master.event_instance_id(), master.clone());
415
416 let diff = CalendarDiff::compute(
417 vec![local_master, local_override],
418 vec![RemoteEvent::new(master)],
419 &sync_bases,
420 &DateRange::default(),
421 );
422
423 assert_eq!(diff.outgoing, vec![EventChange::Create(override_event)]);
424 assert_eq!(diff.incoming, vec![]);
425 }
426
427 #[test]
428 fn orphan_rule_requires_synced_master_evidence() {
429 let (_tmp, calendar) = test_calendar();
430 let (_master, override_event) = recurring_series();
431 let local_override = calendar.create_event(override_event.clone()).unwrap();
432
433 let diff = CalendarDiff::compute(
434 vec![local_override],
435 vec![],
436 &SyncBases::new(),
437 &DateRange::default(),
438 );
439
440 assert_eq!(diff.outgoing, vec![EventChange::Create(override_event)]);
441 assert_eq!(diff.incoming, vec![]);
442 }
443
444 #[test]
445 fn orphan_rule_is_conservative_when_local_master_is_out_of_range() {
446 let (_tmp, calendar) = test_calendar();
447 let (mut master, mut override_event) = recurring_series();
448 master.recurrence = Some(Recurrence::new("FREQ=DAILY;COUNT=2"));
449 override_event.start = EventTime::DateTimeFloating(
450 chrono::NaiveDate::from_ymd_opt(2027, 1, 1)
451 .unwrap()
452 .and_hms_opt(12, 0, 0)
453 .unwrap(),
454 );
455
456 let local_master = calendar.create_event(master.clone()).unwrap();
457 let local_override = calendar.create_event(override_event.clone()).unwrap();
458
459 let mut sync_bases = SyncBases::new();
460 sync_bases.insert_event_base(master.event_instance_id(), master);
461
462 let range = DateRange {
463 from: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
464 to: Some(Utc.with_ymd_and_hms(2027, 1, 2, 0, 0, 0).unwrap()),
465 };
466 let diff = CalendarDiff::compute(
467 vec![local_master, local_override],
468 vec![],
469 &sync_bases,
470 &range,
471 );
472
473 assert_eq!(diff.outgoing, vec![EventChange::Create(override_event)]);
474 assert_eq!(diff.incoming, vec![]);
475 }
476
477 #[test]
478 fn deleted_local_event_becomes_outgoing_delete() {
479 let remote_event = test_event();
480
481 let mut synced_ids = SyncBases::new();
482 synced_ids.insert_known_event_id(remote_event.event_instance_id());
483
484 let local_events = vec![];
485 let remote_events = vec![RemoteEvent::new(remote_event.clone())];
486
487 let diff = CalendarDiff::compute(
488 local_events,
489 remote_events,
490 &synced_ids,
491 &DateRange::default(),
492 );
493
494 assert_eq!(diff.outgoing, vec![EventChange::Delete(remote_event)]);
495 assert_eq!(diff.incoming, vec![]);
496 }
497
498 #[test]
499 fn invitee_seeing_cancellation_gets_status_flip_update() {
500 let (_tmp, calendar) = test_calendar();
503 let event = test_event();
504 let calendar_event = calendar.create_event(event.clone()).unwrap();
505
506 let mut cancelled = event.clone();
507 cancelled.status = Status::Cancelled;
508 cancelled.last_modified = Some(Utc.with_ymd_and_hms(3000, 1, 1, 0, 0, 0).unwrap());
512
513 let diff = CalendarDiff::compute(
514 vec![calendar_event],
515 vec![RemoteEvent::new(cancelled.clone())],
516 &SyncBases::new(),
517 &DateRange::default(),
518 );
519
520 assert_eq!(diff.outgoing, vec![]);
521 assert_eq!(
522 diff.incoming,
523 vec![EventChange::Update {
524 from: event,
525 to: cancelled,
526 }]
527 );
528 }
529
530 #[test]
531 fn missing_local_cancelled_remote_is_skipped() {
532 let mut cancelled = test_event();
536 cancelled.status = Status::Cancelled;
537
538 let mut synced_ids = SyncBases::new();
539 synced_ids.insert_known_event_id(cancelled.event_instance_id());
540
541 let diff = CalendarDiff::compute(
542 vec![],
543 vec![RemoteEvent::new(cancelled)],
544 &synced_ids,
545 &DateRange::default(),
546 );
547
548 assert_eq!(diff.outgoing, vec![]);
549 assert_eq!(diff.incoming, vec![]);
550 }
551
552 #[test]
553 fn cancelled_instance_of_local_master_becomes_incoming_create() {
554 let (_tmp, calendar) = test_calendar();
555 let mut master = test_event();
556 master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
557 let calendar_event = calendar.create_event(master.clone()).unwrap();
558
559 let mut cancelled_instance = master.clone();
560 cancelled_instance.recurrence = None;
561 cancelled_instance.recurrence_id =
562 Some(RecurrenceId::from_event_time(master.start.clone()));
563 cancelled_instance.status = Status::Cancelled;
564
565 let diff = CalendarDiff::compute(
566 vec![calendar_event],
567 vec![
568 RemoteEvent::new(master),
569 RemoteEvent::new(cancelled_instance.clone()),
570 ],
571 &SyncBases::new(),
572 &DateRange::default(),
573 );
574
575 assert_eq!(diff.outgoing, vec![]);
576 assert_eq!(diff.incoming, vec![EventChange::Create(cancelled_instance)]);
577 }
578
579 #[test]
580 fn first_sync_pulls_master_and_cancelled_instance() {
581 let mut master = test_event();
582 master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
583
584 let mut cancelled_instance = master.clone();
585 cancelled_instance.recurrence = None;
586 cancelled_instance.recurrence_id =
587 Some(RecurrenceId::from_event_time(master.start.clone()));
588 cancelled_instance.status = Status::Cancelled;
589
590 let diff = CalendarDiff::compute(
591 vec![],
592 vec![
593 RemoteEvent::new(master.clone()),
594 RemoteEvent::new(cancelled_instance.clone()),
595 ],
596 &SyncBases::new(),
597 &DateRange::default(),
598 );
599
600 assert_eq!(diff.outgoing, vec![]);
601 assert_eq!(
602 diff.incoming,
603 vec![
604 EventChange::Create(master),
605 EventChange::Create(cancelled_instance),
606 ]
607 );
608 }
609
610 #[test]
611 fn cancelled_instance_is_skipped_when_local_master_was_deleted() {
612 let mut master = test_event();
613 master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
614
615 let mut cancelled_instance = master.clone();
616 cancelled_instance.recurrence = None;
617 cancelled_instance.recurrence_id =
618 Some(RecurrenceId::from_event_time(master.start.clone()));
619 cancelled_instance.status = Status::Cancelled;
620
621 let mut sync_bases = SyncBases::new();
622 sync_bases.insert_known_event_id(master.event_instance_id());
623
624 let diff = CalendarDiff::compute(
625 vec![],
626 vec![
627 RemoteEvent::new(master.clone()),
628 RemoteEvent::new(cancelled_instance),
629 ],
630 &sync_bases,
631 &DateRange::default(),
632 );
633
634 assert_eq!(diff.outgoing, vec![EventChange::Delete(master)]);
635 assert_eq!(diff.incoming, vec![]);
636 }
637
638 #[test]
639 fn cancelled_instance_is_skipped_when_remote_master_is_cancelled() {
640 let mut master = test_event();
641 master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
642 master.status = Status::Cancelled;
643
644 let mut cancelled_instance = master.clone();
645 cancelled_instance.recurrence = None;
646 cancelled_instance.recurrence_id =
647 Some(RecurrenceId::from_event_time(master.start.clone()));
648
649 let diff = CalendarDiff::compute(
650 vec![],
651 vec![
652 RemoteEvent::new(master),
653 RemoteEvent::new(cancelled_instance),
654 ],
655 &SyncBases::new(),
656 &DateRange::default(),
657 );
658
659 assert_eq!(diff.outgoing, vec![]);
660 assert_eq!(diff.incoming, vec![]);
661 }
662
663 #[test]
664 fn both_cancelled_with_field_drift_is_skipped() {
665 let (_tmp, calendar) = test_calendar();
669 let mut local = test_event();
670 local.status = Status::Cancelled;
671 let calendar_event = calendar.create_event(local.clone()).unwrap();
672
673 let mut remote = local.clone();
674 remote.summary = Some("Now with extra data".into());
675 remote.location = Some("Somewhere".into());
676
677 let diff = CalendarDiff::compute(
678 vec![calendar_event],
679 vec![RemoteEvent::new(remote)],
680 &SyncBases::new(),
681 &DateRange::default(),
682 );
683
684 assert_eq!(diff.outgoing, vec![]);
685 assert_eq!(diff.incoming, vec![]);
686 }
687
688 #[test]
689 fn expired_recurring_event_not_flagged_as_delete() {
690 let (_tmp, calendar) = test_calendar();
693
694 let mut event = Event::new(
695 "Old recurring",
696 EventTime::Date(chrono::NaiveDate::from_ymd_opt(1968, 5, 8).unwrap()),
697 );
698 event.recurrence = Some(crate::event::Recurrence::new(
699 "FREQ=YEARLY;UNTIL=20080507T120000Z",
700 ));
701 let calendar_event = calendar.create_event(event.clone()).unwrap();
702
703 let mut synced_ids = SyncBases::new();
704 synced_ids.insert_known_event_id(event.event_instance_id());
705
706 let range = DateRange {
707 from: Some(Utc.with_ymd_and_hms(2025, 5, 14, 0, 0, 0).unwrap()),
708 to: Some(Utc.with_ymd_and_hms(2027, 5, 14, 0, 0, 0).unwrap()),
709 };
710
711 let diff = CalendarDiff::compute(vec![calendar_event], vec![], &synced_ids, &range);
712
713 assert_eq!(diff.outgoing, vec![]);
714 assert_eq!(diff.incoming, vec![]);
715 }
716
717 #[test]
718 fn deleted_remote_event_becomes_incoming_delete() {
719 let (_tmp, calendar_event) = test_calendar_event();
720 let local_event = calendar_event.event().clone();
721
722 let mut synced_ids = SyncBases::new();
723 synced_ids.insert_known_event_id(local_event.event_instance_id());
724
725 let local_events = vec![calendar_event];
726 let remote_events = vec![];
727
728 let diff = CalendarDiff::compute(
729 local_events,
730 remote_events,
731 &synced_ids,
732 &DateRange::default(),
733 );
734
735 assert_eq!(diff.outgoing, vec![]);
736 assert_eq!(diff.incoming, vec![EventChange::Delete(local_event)]);
737 }
738
739 #[test]
740 fn event_base_marks_event_as_previously_synced() {
741 let (_tmp, calendar_event) = test_calendar_event();
742 let local_event = calendar_event.event().clone();
743 let id = local_event.event_instance_id();
744
745 let mut sync_bases = SyncBases::new();
746 sync_bases.insert_event_base(id, local_event.clone());
747
748 let diff = CalendarDiff::compute(
749 vec![calendar_event],
750 vec![],
751 &sync_bases,
752 &DateRange::default(),
753 );
754
755 assert_eq!(diff.outgoing, vec![]);
756 assert_eq!(diff.incoming, vec![EventChange::Delete(local_event)]);
757 }
758
759 #[test]
760 fn updated_local_event_becomes_outgoing_update() {
761 let (_tmp, calendar) = test_calendar();
762 let remote_event = test_event();
763
764 let mut local_event = remote_event.clone();
765 local_event.summary = Some("Updated Test Event".to_string());
766 let calendar_event = calendar.create_event(local_event.clone()).unwrap();
767
768 let mut synced_ids = SyncBases::new();
769 synced_ids.insert_known_event_id(remote_event.event_instance_id());
770
771 let local_events = vec![calendar_event];
772 let remote_events = vec![RemoteEvent::new(remote_event.clone())];
773
774 let diff = CalendarDiff::compute(
775 local_events,
776 remote_events,
777 &synced_ids,
778 &DateRange::default(),
779 );
780
781 assert_eq!(
782 diff.outgoing,
783 vec![EventChange::Update {
784 from: remote_event,
785 to: local_event,
786 }]
787 );
788 assert_eq!(diff.incoming, vec![]);
789 }
790
791 #[test]
792 fn higher_remote_sequence_breaks_conflict_without_modified_time() {
793 let (_tmp, calendar) = test_calendar();
794 let mut base = test_event();
795 base.sequence = 1;
796
797 let mut local = base.clone();
798 local.summary = Some("Edited locally".to_string());
799 local.sequence = 2;
800 let calendar_event = calendar.create_event(local.clone()).unwrap();
801
802 let mut remote = base.clone();
803 remote.summary = Some("Edited remotely".to_string());
804 remote.sequence = 3;
805 remote.last_modified = None;
806
807 let mut sync_bases = SyncBases::new();
808 sync_bases.insert_event_base(base.event_instance_id(), base);
809
810 let diff = CalendarDiff::compute(
811 vec![calendar_event],
812 vec![RemoteEvent::new(remote.clone())],
813 &sync_bases,
814 &DateRange::default(),
815 );
816
817 assert_eq!(diff.outgoing, vec![]);
818 assert_eq!(
819 diff.incoming,
820 vec![EventChange::Update {
821 from: local,
822 to: remote,
823 }]
824 );
825 }
826
827 #[test]
828 fn remote_update_without_modified_time_is_pulled_when_local_matches_base() {
829 let (_tmp, calendar) = test_calendar();
830 let base = test_event();
831 let calendar_event = calendar.create_event(base.clone()).unwrap();
832 assert!(calendar_event.modified_at().is_some());
833
834 let mut remote = base.clone();
835 remote.summary = Some("Edited remotely".to_string());
836 remote.last_modified = None;
837
838 let mut sync_bases = SyncBases::new();
839 sync_bases.insert_event_base(base.event_instance_id(), base.clone());
840
841 let diff = CalendarDiff::compute(
842 vec![calendar_event],
843 vec![RemoteEvent::new(remote.clone())],
844 &sync_bases,
845 &DateRange::default(),
846 );
847
848 assert_eq!(diff.outgoing, vec![]);
849 assert_eq!(
850 diff.incoming,
851 vec![EventChange::Update {
852 from: base,
853 to: remote,
854 }]
855 );
856 }
857
858 #[test]
859 fn remote_update_is_pulled_when_unchanged_local_file_has_newer_mtime() {
860 let (_tmp, calendar) = test_calendar();
861 let mut local = test_event();
862 local.last_modified = Some(Utc.with_ymd_and_hms(2026, 3, 5, 8, 45, 16).unwrap());
863 let calendar_event = calendar.create_event(local.clone()).unwrap();
864
865 let mut base = local.clone();
866 base.last_modified = None;
867
868 let mut remote = base.clone();
869 remote.summary = Some("Edited remotely".to_string());
870 remote.last_modified = Some(Utc.with_ymd_and_hms(2025, 6, 9, 10, 42, 20).unwrap());
871
872 assert!(calendar_event.modified_at() > remote.last_modified);
873
874 let mut sync_bases = SyncBases::new();
875 sync_bases.insert_event_base(base.event_instance_id(), base.clone());
876
877 let diff = CalendarDiff::compute(
878 vec![calendar_event],
879 vec![RemoteEvent::new(remote.clone())],
880 &sync_bases,
881 &DateRange::default(),
882 );
883
884 assert_eq!(diff.outgoing, vec![]);
885 assert_eq!(
886 diff.incoming,
887 vec![EventChange::Update {
888 from: local,
889 to: remote,
890 }]
891 );
892 }
893
894 #[test]
895 fn local_update_is_pushed_when_unchanged_remote_has_newer_modified_time() {
896 let (_tmp, calendar) = test_calendar();
897 let base = test_event();
898
899 let mut local = base.clone();
900 local.summary = Some("Edited locally".to_string());
901 local.last_modified = Some(Utc.with_ymd_and_hms(2025, 6, 9, 10, 42, 20).unwrap());
902 let calendar_event = calendar.create_event(local.clone()).unwrap();
903
904 let mut remote = base.clone();
905 remote.last_modified = Some(Utc.with_ymd_and_hms(2026, 3, 5, 8, 45, 16).unwrap());
906 assert!(calendar_event.modified_at() < remote.last_modified);
907
908 let mut sync_bases = SyncBases::new();
909 sync_bases.insert_event_base(base.event_instance_id(), base);
910
911 let diff = CalendarDiff::compute(
912 vec![calendar_event],
913 vec![RemoteEvent::new(remote.clone())],
914 &sync_bases,
915 &DateRange::default(),
916 );
917
918 assert_eq!(diff.incoming, vec![]);
919 assert_eq!(
920 diff.outgoing,
921 vec![EventChange::Update {
922 from: remote,
923 to: local,
924 }]
925 );
926 }
927
928 #[test]
929 fn sync_metadata_only_differences_produce_no_diff() {
930 let (_tmp, calendar) = test_calendar();
933 let local_event = test_event();
934
935 let mut remote_event = local_event.clone();
936 remote_event.last_modified = Some(Utc::now() + chrono::Duration::days(1));
937 remote_event.sequence += 1;
938
939 let calendar_event = calendar.create_event(local_event.clone()).unwrap();
940
941 let mut synced_ids = SyncBases::new();
942 synced_ids.insert_known_event_id(local_event.event_instance_id());
943
944 let diff = CalendarDiff::compute(
945 vec![calendar_event],
946 vec![RemoteEvent::new(remote_event)],
947 &synced_ids,
948 &DateRange::default(),
949 );
950
951 assert_eq!(diff.outgoing, vec![]);
952 assert_eq!(diff.incoming, vec![]);
953 }
954
955 #[test]
956 fn equal_timestamps_with_field_drift_resolve_to_pull() {
957 let (_tmp, calendar) = test_calendar();
963 let when = Utc.with_ymd_and_hms(2026, 5, 15, 13, 12, 52).unwrap();
964
965 let mut local_event = test_event();
966 local_event.last_modified = Some(when);
967 let calendar_event = calendar.create_event(local_event.clone()).unwrap();
968
969 let mut remote_event = local_event.clone();
970 remote_event.visibility = Some(crate::event::Visibility::Private);
971
972 let mut synced_ids = SyncBases::new();
973 synced_ids.insert_known_event_id(local_event.event_instance_id());
974
975 let diff = CalendarDiff::compute(
976 vec![calendar_event],
977 vec![RemoteEvent::new(remote_event.clone())],
978 &synced_ids,
979 &DateRange::default(),
980 );
981
982 assert_eq!(diff.outgoing, vec![]);
983 assert_eq!(
984 diff.incoming,
985 vec![EventChange::Update {
986 from: local_event,
987 to: remote_event,
988 }]
989 );
990 }
991
992 #[test]
993 fn unspecified_local_visibility_resolves_to_pull_even_when_local_mtime_is_newer() {
994 let (_tmp, calendar) = test_calendar();
1000
1001 let mut local_event = test_event();
1002 local_event.last_modified = Some(Utc.with_ymd_and_hms(2026, 3, 5, 8, 45, 16).unwrap());
1003 assert_eq!(local_event.visibility, None);
1004 let calendar_event = calendar.create_event(local_event.clone()).unwrap();
1005
1006 let mut remote_event = local_event.clone();
1007 remote_event.visibility = Some(crate::event::Visibility::Private);
1008 remote_event.last_modified = Some(Utc.with_ymd_and_hms(2025, 6, 9, 10, 42, 20).unwrap());
1009
1010 let mut synced_ids = SyncBases::new();
1011 synced_ids.insert_known_event_id(local_event.event_instance_id());
1012
1013 let diff = CalendarDiff::compute(
1014 vec![calendar_event],
1015 vec![RemoteEvent::new(remote_event.clone())],
1016 &synced_ids,
1017 &DateRange::default(),
1018 );
1019
1020 assert_eq!(diff.outgoing, vec![]);
1021 assert_eq!(
1022 diff.incoming,
1023 vec![EventChange::Update {
1024 from: local_event,
1025 to: remote_event,
1026 }]
1027 );
1028 }
1029
1030 #[test]
1031 fn outgoing_update_keeps_remote_visibility_when_local_is_unspecified() {
1032 let (_tmp, calendar) = test_calendar();
1036
1037 let mut local_event = test_event();
1038 local_event.summary = Some("Edited locally".to_string());
1039 local_event.last_modified = Some(Utc.with_ymd_and_hms(2026, 3, 5, 8, 45, 16).unwrap());
1040 let calendar_event = calendar.create_event(local_event.clone()).unwrap();
1041
1042 let mut remote_event = local_event.clone();
1043 remote_event.summary = Some("Test Event".to_string());
1044 remote_event.visibility = Some(crate::event::Visibility::Private);
1045 remote_event.last_modified = Some(Utc.with_ymd_and_hms(2025, 6, 9, 10, 42, 20).unwrap());
1046
1047 let mut synced_ids = SyncBases::new();
1048 synced_ids.insert_known_event_id(local_event.event_instance_id());
1049
1050 let diff = CalendarDiff::compute(
1051 vec![calendar_event],
1052 vec![RemoteEvent::new(remote_event)],
1053 &synced_ids,
1054 &DateRange::default(),
1055 );
1056
1057 assert_eq!(diff.incoming, vec![]);
1058 match diff.outgoing.as_slice() {
1059 [EventChange::Update { to, .. }] => {
1060 assert_eq!(to.summary.as_deref(), Some("Edited locally"));
1061 assert_eq!(to.visibility, Some(crate::event::Visibility::Private));
1062 }
1063 other => panic!("expected one outgoing Update, got {other:?}"),
1064 }
1065 }
1066
1067 #[test]
1068 fn x_property_change_surfaces_as_diff() {
1069 let (_tmp, calendar) = test_calendar();
1070 let mut local_event = test_event();
1071 local_event
1072 .x_properties
1073 .push(crate::event::XProperty::new("X-GOOGLE-COLOR-ID", "5"));
1074
1075 let mut remote_event = local_event.clone();
1076 remote_event.x_properties = vec![crate::event::XProperty::new("X-GOOGLE-COLOR-ID", "9")];
1077 remote_event.last_modified = Some(Utc::now() + chrono::Duration::days(1));
1078
1079 let calendar_event = calendar.create_event(local_event.clone()).unwrap();
1080
1081 let mut synced_ids = SyncBases::new();
1082 synced_ids.insert_known_event_id(local_event.event_instance_id());
1083
1084 let diff = CalendarDiff::compute(
1085 vec![calendar_event],
1086 vec![RemoteEvent::new(remote_event.clone())],
1087 &synced_ids,
1088 &DateRange::default(),
1089 );
1090
1091 assert_eq!(diff.outgoing, vec![]);
1092 assert_eq!(
1093 diff.incoming,
1094 vec![EventChange::Update {
1095 from: local_event,
1096 to: remote_event,
1097 }]
1098 );
1099 }
1100
1101 #[test]
1102 fn x_properties_compare_equal_regardless_of_order() {
1103 let mut event_a = test_event();
1107 event_a.x_properties = vec![
1108 crate::event::XProperty::new("X-GOOGLE-EVENT-ID", "abc"),
1109 crate::event::XProperty::new("X-GOOGLE-COLOR-ID", "5"),
1110 ];
1111
1112 let mut event_b = event_a.clone();
1113 event_b.x_properties = vec![
1114 crate::event::XProperty::new("X-GOOGLE-COLOR-ID", "5"),
1115 crate::event::XProperty::new("X-GOOGLE-EVENT-ID", "abc"),
1116 ];
1117
1118 assert_eq!(event_a, event_b);
1119 }
1120
1121 #[test]
1122 fn updated_remote_event_becomes_incoming_update() {
1123 let (_tmp, calendar) = test_calendar();
1124 let local_event = test_event();
1125
1126 let mut remote_event = local_event.clone();
1127 remote_event.summary = Some("Updated Test Event".to_string());
1128 remote_event.last_modified = Some(Utc::now() + chrono::Duration::days(1));
1129
1130 let calendar_event = calendar.create_event(local_event.clone()).unwrap();
1131
1132 let mut synced_ids = SyncBases::new();
1133 synced_ids.insert_known_event_id(local_event.event_instance_id());
1134
1135 let local_events = vec![calendar_event];
1136 let remote_events = vec![RemoteEvent::new(remote_event.clone())];
1137
1138 let diff = CalendarDiff::compute(
1139 local_events,
1140 remote_events,
1141 &synced_ids,
1142 &DateRange::default(),
1143 );
1144
1145 assert_eq!(diff.outgoing, vec![]);
1146 assert_eq!(
1147 diff.incoming,
1148 vec![EventChange::Update {
1149 from: local_event,
1150 to: remote_event,
1151 }]
1152 );
1153 }
1154
1155 #[test]
1156 fn local_event_outside_window_is_not_flagged_for_delete() {
1157 let (_tmp, calendar) = test_calendar();
1160
1161 let mut old_event = test_event();
1162 old_event.start =
1163 EventTime::DateTimeUtc(Utc.with_ymd_and_hms(2020, 1, 1, 9, 0, 0).unwrap());
1164 old_event.end = Some(EventTime::DateTimeUtc(
1165 Utc.with_ymd_and_hms(2020, 1, 1, 10, 0, 0).unwrap(),
1166 ));
1167 let calendar_event = calendar.create_event(old_event.clone()).unwrap();
1168
1169 let mut synced_ids = SyncBases::new();
1170 synced_ids.insert_known_event_id(old_event.event_instance_id());
1171
1172 let range = DateRange {
1173 from: Some(Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()),
1174 to: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
1175 };
1176
1177 let diff = CalendarDiff::compute(vec![calendar_event], vec![], &synced_ids, &range);
1178
1179 assert_eq!(diff.outgoing, vec![]);
1180 assert_eq!(diff.incoming, vec![]);
1181 }
1182
1183 #[test]
1184 fn local_event_inside_window_is_flagged_for_delete_when_remote_drops_it() {
1185 let (_tmp, calendar) = test_calendar();
1186
1187 let mut event = test_event();
1188 event.start = EventTime::DateTimeUtc(Utc.with_ymd_and_hms(2026, 6, 15, 9, 0, 0).unwrap());
1189 event.end = Some(EventTime::DateTimeUtc(
1190 Utc.with_ymd_and_hms(2026, 6, 15, 10, 0, 0).unwrap(),
1191 ));
1192 let calendar_event = calendar.create_event(event.clone()).unwrap();
1193
1194 let mut synced_ids = SyncBases::new();
1195 synced_ids.insert_known_event_id(event.event_instance_id());
1196
1197 let range = DateRange {
1198 from: Some(Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()),
1199 to: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
1200 };
1201
1202 let diff = CalendarDiff::compute(vec![calendar_event], vec![], &synced_ids, &range);
1203
1204 assert_eq!(diff.outgoing, vec![]);
1205 assert_eq!(diff.incoming, vec![EventChange::Delete(event)]);
1206 }
1207
1208 #[test]
1209 fn one_sided_window_leaves_out_of_range_local_events_untouched() {
1210 let boundary = Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap();
1211 for (year, range) in [
1212 (
1213 2028,
1214 DateRange {
1215 from: None,
1216 to: Some(boundary),
1217 },
1218 ),
1219 (
1220 2026,
1221 DateRange {
1222 from: Some(boundary),
1223 to: None,
1224 },
1225 ),
1226 ] {
1227 for synced in [false, true] {
1228 for recurring in [false, true] {
1229 let (_tmp, calendar) = test_calendar();
1230 let mut event = test_event();
1231 event.start = EventTime::DateTimeUtc(
1232 Utc.with_ymd_and_hms(year, 2, 25, 15, 45, 0).unwrap(),
1233 );
1234 event.end = None;
1235 if recurring {
1236 event.recurrence = Some(Recurrence::new("FREQ=DAILY;COUNT=2"));
1237 }
1238 let local = calendar.create_event(event.clone()).unwrap();
1239 let mut bases = SyncBases::new();
1240 if synced {
1241 bases.insert_known_event_id(event.event_instance_id());
1242 }
1243
1244 let diff = CalendarDiff::compute(vec![local], vec![], &bases, &range);
1245 assert!(
1246 diff.is_empty(),
1247 "year={year}, synced={synced}, recurring={recurring}"
1248 );
1249 }
1250 }
1251 }
1252 }
1253
1254 #[test]
1255 fn one_sided_window_still_detects_remote_deletions_in_range() {
1256 for range in [
1257 DateRange {
1258 from: None,
1259 to: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
1260 },
1261 DateRange {
1262 from: Some(Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()),
1263 to: None,
1264 },
1265 ] {
1266 let (_tmp, local) = test_calendar_event();
1267 let event = local.event().clone();
1268 let mut bases = SyncBases::new();
1269 bases.insert_known_event_id(event.event_instance_id());
1270
1271 let diff = CalendarDiff::compute(vec![local], vec![], &bases, &range);
1272 assert_eq!(diff.incoming, vec![EventChange::Delete(event)]);
1273 assert!(diff.outgoing.is_empty());
1274 }
1275 }
1276
1277 #[test]
1278 fn one_sided_window_does_not_treat_out_of_range_master_as_deleted() {
1279 for (year, range) in [
1280 (
1281 2025,
1282 DateRange {
1283 from: None,
1284 to: Some(Utc.with_ymd_and_hms(2025, 12, 31, 0, 0, 0).unwrap()),
1285 },
1286 ),
1287 (
1288 2027,
1289 DateRange {
1290 from: Some(Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()),
1291 to: None,
1292 },
1293 ),
1294 ] {
1295 let (_tmp, calendar) = test_calendar();
1296 let (mut master, mut override_event) = recurring_series();
1297 master.recurrence = Some(Recurrence::new("FREQ=DAILY;COUNT=2"));
1298 override_event.start =
1299 EventTime::DateTimeUtc(Utc.with_ymd_and_hms(year, 6, 1, 12, 0, 0).unwrap());
1300 override_event.end = None;
1301 let local_master = calendar.create_event(master.clone()).unwrap();
1302 let local_override = calendar.create_event(override_event.clone()).unwrap();
1303 let mut bases = SyncBases::new();
1304 bases.insert_event_base(master.event_instance_id(), master);
1305
1306 let diff =
1307 CalendarDiff::compute(vec![local_master, local_override], vec![], &bases, &range);
1308 assert!(diff.incoming.is_empty());
1309 assert_eq!(diff.outgoing, vec![EventChange::Create(override_event)]);
1310 }
1311 }
1312}