1mod error;
2
3use std::collections::HashMap;
4use std::collections::hash_map::Entry;
5
6use crate::calendar::{CalendarError, SyncBases};
7use crate::diff::EventChange;
8use crate::event::EventInstanceId;
9use crate::{Calendar, CalendarDiff, CalendarEvent, DateRange, Event, Remote, RemoteEvent};
10pub use error::ConnectionError;
11
12pub struct Connection {
14 local: Calendar,
15 remote: Remote,
16}
17
18impl Connection {
19 pub fn new(local: Calendar, remote: Remote) -> Self {
20 Self { local, remote }
21 }
22
23 pub fn local(&self) -> &Calendar {
24 &self.local
25 }
26
27 pub fn remote(&self) -> &Remote {
28 &self.remote
29 }
30
31 pub fn read_only(&self) -> bool {
32 self.local
33 .config()
34 .and_then(|c| c.read_only())
35 .unwrap_or(false)
36 }
37
38 pub async fn diff(&mut self, range: &DateRange) -> Result<CalendarDiff, ConnectionError> {
39 let local_events = self.local().events()?;
40 let remote_events = self.remote().list_events(range).await?;
41
42 let backfill = bases_to_backfill(
46 &local_events,
47 &remote_events,
48 self.local.state().sync_bases(),
49 );
50 if !backfill.is_empty() {
51 self.local.record_sync_bases(backfill)?;
52 }
53
54 let sync_bases = self.local().state().sync_bases();
55
56 let mut diff = CalendarDiff::compute(local_events, remote_events, sync_bases, range);
57
58 if self.read_only() {
59 diff.discard_outgoing();
60 }
61
62 Ok(diff)
63 }
64
65 pub fn apply_incoming_diff(&mut self, diff: &CalendarDiff) -> Result<(), ConnectionError> {
67 let mut events_by_instance_id: HashMap<EventInstanceId, CalendarEvent> = self
68 .local
69 .events()?
70 .into_iter()
71 .map(|e| (e.event().event_instance_id(), e))
72 .collect();
73
74 let mut sync_bases = Vec::new();
75
76 let loop_result = pull_incoming_changes(
79 &self.local,
80 diff,
81 &mut events_by_instance_id,
82 &mut sync_bases,
83 );
84
85 let record_result = self.local.record_sync_bases(sync_bases);
86
87 loop_result?;
88 record_result?;
89 Ok(())
90 }
91
92 pub async fn apply_outgoing_diff(
94 &mut self,
95 diff: &CalendarDiff,
96 ) -> Result<(), ConnectionError> {
97 let mut events_by_instance_id: HashMap<EventInstanceId, CalendarEvent> = self
98 .local
99 .events()?
100 .into_iter()
101 .map(|e| (e.event().event_instance_id(), e))
102 .collect();
103
104 let mut sync_bases = Vec::new();
105
106 let loop_result = push_outgoing_changes(
108 &self.remote,
109 diff,
110 &mut events_by_instance_id,
111 &mut sync_bases,
112 )
113 .await;
114
115 let record_result = self.local.record_sync_bases(sync_bases);
116
117 loop_result?;
118 record_result?;
119 Ok(())
120 }
121
122 pub fn discard_outgoing_diff(&self, diff: &CalendarDiff) -> Result<(), ConnectionError> {
124 let mut events_by_instance_id: HashMap<EventInstanceId, CalendarEvent> = self
125 .local
126 .events()?
127 .into_iter()
128 .map(|e| (e.event().event_instance_id(), e))
129 .collect();
130
131 for change in diff.outgoing() {
132 match change {
133 EventChange::Create(event) => {
134 if let Some(cal_event) =
135 events_by_instance_id.remove(&event.event_instance_id())
136 {
137 cal_event.delete().map_err(CalendarError::from)?;
138 }
139 }
140 EventChange::Update { from, to } => {
141 if let Some(cal_event) = events_by_instance_id.get_mut(&to.event_instance_id())
142 {
143 cal_event
144 .update(from.clone())
145 .map_err(CalendarError::from)?;
146 }
147 }
148 EventChange::Delete(event) => {
149 let cal_event = self.local.create_event(event.clone())?;
150 events_by_instance_id.insert(cal_event.event().event_instance_id(), cal_event);
151 }
152 }
153 }
154
155 Ok(())
156 }
157}
158
159fn bases_to_backfill(
164 local_events: &[CalendarEvent],
165 remote_events: &[RemoteEvent],
166 sync_bases: &SyncBases,
167) -> Vec<Event> {
168 let local_by_id: HashMap<_, _> = local_events
169 .iter()
170 .map(|e| (e.event().event_instance_id(), e.event()))
171 .collect();
172
173 let mut backfill = Vec::new();
174
175 for remote in remote_events {
176 let remote = remote.event();
177 let id = remote.event_instance_id();
178
179 let in_sync = local_by_id.get(&id).is_some_and(|local| *local == remote);
180
181 let base_is_current = match sync_bases.get(&id) {
182 None => continue,
183 Some(Some(base)) => base.as_ref() == remote,
184 Some(None) => false,
185 };
186
187 if in_sync && !base_is_current {
188 backfill.push(remote.clone());
189 }
190 }
191
192 backfill
193}
194
195fn pull_incoming_changes(
196 local: &Calendar,
197 diff: &CalendarDiff,
198 events_by_instance_id: &mut HashMap<EventInstanceId, CalendarEvent>,
199 sync_bases: &mut Vec<Event>,
200) -> Result<(), ConnectionError> {
201 for change in diff.incoming() {
202 match change {
203 EventChange::Create(event) => {
204 match events_by_instance_id.entry(event.event_instance_id()) {
206 Entry::Vacant(slot) => {
207 slot.insert(local.create_event(event.clone())?);
208 }
209 Entry::Occupied(slot) if slot.get().event() != event => continue,
210 Entry::Occupied(_) => {}
211 }
212 sync_bases.push(event.clone());
214 }
215 EventChange::Update { to, .. } => {
216 if let Some(cal_event) = events_by_instance_id.get_mut(&to.event_instance_id()) {
217 cal_event.update(to.clone()).map_err(CalendarError::from)?;
218 }
219 sync_bases.push(to.clone());
220 }
221 EventChange::Delete(event) => {
222 if let Some(cal_event) = events_by_instance_id.remove(&event.event_instance_id()) {
223 cal_event.delete().map_err(CalendarError::from)?;
224 }
225 }
226 }
227 }
228
229 Ok(())
230}
231
232async fn push_outgoing_changes(
233 remote: &Remote,
234 diff: &CalendarDiff,
235 events_by_instance_id: &mut HashMap<EventInstanceId, CalendarEvent>,
236 sync_bases: &mut Vec<Event>,
237) -> Result<(), ConnectionError> {
238 for change in diff.outgoing() {
239 if let Some(remote_event) = remote.apply_change(change).await? {
240 let returned_event = remote_event.event();
241
242 let original_event_id = match change {
244 EventChange::Create(event) => event.event_instance_id(),
245 EventChange::Update { to, .. } => to.event_instance_id(),
246 EventChange::Delete(_) => unreachable!("apply_change returns None for Delete"),
247 };
248
249 if let Some(cal_event) = events_by_instance_id.get_mut(&original_event_id) {
250 cal_event
251 .update(returned_event.clone())
252 .map_err(CalendarError::from)?;
253 }
254
255 sync_bases.push(returned_event.clone());
256 }
257 }
258
259 Ok(())
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::diff::EventChange;
266 use crate::event::{EventUid, Recurrence, XProperty};
267 use crate::provider::mock_provider::MockProvider;
268 use crate::test_utils::{
269 incoming_create_diff, incoming_delete_diff, incoming_update_diff, outgoing_create_diff,
270 outgoing_delete_diff, outgoing_update_diff, test_caldir, test_event, test_mock_provider,
271 test_remote_config, test_remote_params,
272 };
273 use crate::{CalendarConfig, rpc};
274 use pretty_assertions::assert_eq;
275
276 fn calendar_config(read_only: Option<bool>) -> CalendarConfig {
277 CalendarConfig::new(
278 Some("Read-only test".to_string()),
279 None,
280 read_only,
281 Some(test_remote_config("test-provider")),
282 )
283 }
284
285 fn writable_connection() -> (tempfile::TempDir, MockProvider, Connection) {
286 let (tmp, caldir) = test_caldir();
287 let calendar = caldir
288 .create_calendar("writable-cal", Some(calendar_config(Some(false))))
289 .unwrap();
290 let mock = test_mock_provider();
291 let remote = Remote::new(mock.provider(), test_remote_params());
292 let connection = Connection::new(calendar, remote);
293 (tmp, mock, connection)
294 }
295
296 #[tokio::test]
297 async fn diff_discards_outgoing_when_read_only() {
298 let (_tmp, caldir) = test_caldir();
299 let calendar = caldir
300 .create_calendar("read-only-cal", Some(calendar_config(Some(true))))
301 .unwrap();
302 calendar.create_event(test_event()).unwrap();
303
304 let mock = test_mock_provider();
305 mock.reply::<rpc::ListEvents>(vec![]);
306 let remote = Remote::new(mock.provider(), test_remote_params());
307
308 let mut connection = Connection::new(calendar, remote);
309 let diff = connection.diff(&DateRange::default()).await.unwrap();
310
311 assert!(
312 diff.outgoing().is_empty(),
313 "outgoing should be empty for read-only calendars, got {:?}",
314 diff.outgoing()
315 );
316 }
317
318 #[tokio::test]
319 async fn diff_includes_outgoing_when_not_read_only() {
320 let (_tmp, caldir) = test_caldir();
321 let calendar = caldir
322 .create_calendar("writable-cal", Some(calendar_config(Some(false))))
323 .unwrap();
324 let event = test_event();
325 calendar.create_event(event.clone()).unwrap();
326
327 let mock = test_mock_provider();
328 mock.reply::<rpc::ListEvents>(vec![]);
329 let remote = Remote::new(mock.provider(), test_remote_params());
330
331 let mut connection = Connection::new(calendar, remote);
332 let diff = connection.diff(&DateRange::default()).await.unwrap();
333
334 assert_eq!(diff.outgoing(), &[EventChange::Create(event)]);
335 }
336
337 #[tokio::test]
338 async fn diff_backfills_base_for_in_sync_legacy_known_id() {
339 let (_tmp, caldir) = test_caldir();
340 let calendar = caldir
341 .create_calendar("writable-cal", Some(calendar_config(Some(false))))
342 .unwrap();
343 let event = test_event();
344 let id = event.event_instance_id();
345 calendar.create_event(event.clone()).unwrap();
346
347 let state_dir = calendar.path().join(".caldir/state");
349 std::fs::create_dir_all(&state_dir).unwrap();
350 std::fs::write(state_dir.join("known_event_ids"), id.to_string()).unwrap();
351 let calendar = Calendar::load(calendar.path()).unwrap();
352 assert_eq!(calendar.state().sync_base(&id), None);
353
354 let mock = test_mock_provider();
355 mock.reply::<rpc::ListEvents>(vec![event.clone()]);
356 let remote = Remote::new(mock.provider(), test_remote_params());
357
358 let mut connection = Connection::new(calendar, remote);
359 let diff = connection.diff(&DateRange::default()).await.unwrap();
360
361 assert!(diff.is_empty());
362 let reloaded = Calendar::load(connection.local().path()).unwrap();
363 assert_eq!(reloaded.state().sync_base(&id), Some(&event));
364 }
365
366 #[tokio::test]
367 async fn diff_does_not_backfill_base_for_never_synced_pair() {
368 let (_tmp, mock, mut connection) = writable_connection();
369 let event = test_event();
370 let id = event.event_instance_id();
371 connection.local().create_event(event.clone()).unwrap();
372
373 mock.reply::<rpc::ListEvents>(vec![event]);
374 connection.diff(&DateRange::default()).await.unwrap();
375
376 let reloaded = Calendar::load(connection.local().path()).unwrap();
377 assert!(!reloaded.state().synced_event_ids().contains(&id));
378 }
379
380 #[tokio::test]
381 async fn deleted_remote_series_cleans_up_unsynced_local_override() {
382 let (_tmp, mock, mut connection) = writable_connection();
383 let mut master = test_event();
384 master.recurrence = Some(Recurrence::new("FREQ=DAILY"));
385 let mut override_event = master.occurrence_at(master.start.clone());
386 override_event.summary = Some("Overridden occurrence".to_string());
387
388 connection.local().create_event(master.clone()).unwrap();
389 connection
390 .local()
391 .create_event(override_event.clone())
392 .unwrap();
393 connection
394 .local
395 .record_sync_bases([master.clone()])
396 .unwrap();
397
398 mock.reply::<rpc::ListEvents>(vec![]);
399
400 let diff = connection.diff(&DateRange::default()).await.unwrap();
401 assert_eq!(diff.incoming().len(), 2);
402 assert!(diff.incoming().contains(&EventChange::Delete(master)));
403 assert!(
404 diff.incoming()
405 .contains(&EventChange::Delete(override_event))
406 );
407 assert!(diff.outgoing().is_empty());
408
409 connection.apply_incoming_diff(&diff).unwrap();
410 connection.apply_outgoing_diff(&diff).await.unwrap();
411
412 assert!(connection.local().events().unwrap().is_empty());
413 let _: rpc::ListEvents = mock.captured_request();
414 }
415
416 #[tokio::test]
417 async fn apply_incoming_diff_creates_file_for_incoming_create() {
418 let (_tmp, _mock, mut connection) = writable_connection();
419 let event = test_event();
420
421 connection
422 .apply_incoming_diff(&incoming_create_diff(event))
423 .unwrap();
424
425 let expected_path = connection
426 .local()
427 .path()
428 .join("2026-01-01T1200__test-event.ics");
429 assert!(expected_path.is_file());
430 }
431
432 #[tokio::test]
433 async fn apply_incoming_diff_does_not_recreate_existing_event() {
434 let (_tmp, _mock, mut connection) = writable_connection();
435 let event = test_event();
436 let diff = incoming_create_diff(event.clone());
437
438 connection.apply_incoming_diff(&diff).unwrap();
439 connection.apply_incoming_diff(&diff).unwrap();
440 let events = connection.local().events().unwrap();
441 assert_eq!(events.len(), 1);
442 assert_eq!(events[0].event(), &event);
443
444 let mut edited = event.clone();
446 edited.summary = Some("Locally edited".to_string());
447 let mut cal_event = connection.local().events().unwrap().pop().unwrap();
448 cal_event.update(edited.clone()).unwrap();
449
450 connection.apply_incoming_diff(&diff).unwrap();
451 let events = connection.local().events().unwrap();
452 assert_eq!(events.len(), 1);
453 assert_eq!(events[0].event(), &edited);
454 let reloaded = Calendar::load(connection.local().path()).unwrap();
455 assert_eq!(
456 reloaded.state().sync_base(&event.event_instance_id()),
457 Some(&event)
458 );
459 }
460
461 #[tokio::test]
462 async fn overlapping_creates_do_not_push_older_event_over_newer_remote() {
463 let (_tmp, mock, mut connection) = writable_connection();
464 let mut older = test_event();
465 older.last_modified = Some("2026-09-14T08:00:00Z".parse().unwrap());
466 let mut newer = older.clone();
467 newer.summary = Some("Newer remote title".to_string());
468 newer.last_modified = Some("2026-09-14T09:00:00Z".parse().unwrap());
469
470 let older_diff = incoming_create_diff(older.clone());
472 let newer_diff = incoming_create_diff(newer.clone());
473 connection.apply_incoming_diff(&older_diff).unwrap();
474 connection.apply_incoming_diff(&newer_diff).unwrap();
475
476 mock.reply::<rpc::ListEvents>(vec![newer.clone()]);
477 let next_diff = connection.diff(&DateRange::default()).await.unwrap();
478 assert!(next_diff.outgoing().is_empty());
479 assert_eq!(
480 next_diff.incoming(),
481 &[EventChange::Update {
482 from: older,
483 to: newer
484 }]
485 );
486 }
487
488 #[tokio::test]
489 async fn apply_incoming_diff_records_base_for_existing_unsynced_file() {
490 let (_tmp, _mock, mut connection) = writable_connection();
491 let event = test_event();
492 let id = event.event_instance_id();
493
494 connection.local().create_event(event.clone()).unwrap();
496 assert_eq!(connection.local().state().sync_base(&id), None);
497
498 connection
499 .apply_incoming_diff(&incoming_create_diff(event.clone()))
500 .unwrap();
501
502 assert_eq!(connection.local().events().unwrap().len(), 1);
503 let reloaded = Calendar::load(connection.local().path()).unwrap();
504 assert_eq!(reloaded.state().sync_base(&id), Some(&event));
505 }
506
507 #[tokio::test]
508 async fn apply_incoming_diff_updates_file_for_incoming_update() {
509 let (_tmp, _mock, mut connection) = writable_connection();
510 let from = test_event();
511 let cal_event = connection.local().create_event(from.clone()).unwrap();
512 let old_path = cal_event.path().to_path_buf();
513
514 let mut to = from.clone();
515 to.summary = Some("Updated Test Event".to_string());
516
517 connection
518 .apply_incoming_diff(&incoming_update_diff(from, to))
519 .unwrap();
520
521 let new_path = connection
522 .local()
523 .path()
524 .join("2026-01-01T1200__updated-test-event.ics");
525 assert!(new_path.is_file());
526 assert!(!old_path.exists());
527 }
528
529 #[tokio::test]
530 async fn apply_incoming_diff_deletes_file_for_incoming_delete() {
531 let (_tmp, _mock, mut connection) = writable_connection();
532 let event = test_event();
533 let cal_event = connection.local().create_event(event.clone()).unwrap();
534 let path = cal_event.path().to_path_buf();
535
536 connection
537 .apply_incoming_diff(&incoming_delete_diff(event))
538 .unwrap();
539
540 assert!(!path.exists());
541 }
542
543 #[tokio::test]
544 async fn apply_incoming_diff_records_incoming_create_in_state() {
545 let (_tmp, _mock, mut connection) = writable_connection();
546 let event = test_event();
547 let id = event.event_instance_id();
548
549 connection
550 .apply_incoming_diff(&incoming_create_diff(event))
551 .unwrap();
552
553 assert!(connection.local().state().synced_event_ids().contains(&id));
554 }
555
556 #[tokio::test]
557 async fn apply_incoming_diff_persists_base_to_disk() {
558 let (_tmp, _mock, mut connection) = writable_connection();
559 let event = test_event();
560 let id = event.event_instance_id();
561
562 connection
563 .apply_incoming_diff(&incoming_create_diff(event.clone()))
564 .unwrap();
565
566 let reloaded = Calendar::load(connection.local().path()).unwrap();
567 assert_eq!(reloaded.state().sync_base(&id), Some(&event));
568 }
569
570 #[tokio::test]
571 async fn apply_incoming_diff_does_not_record_deletes_in_state() {
572 let (_tmp, _mock, mut connection) = writable_connection();
573 let event = test_event();
574 let id = event.event_instance_id();
575 connection.local().create_event(event.clone()).unwrap();
576
577 connection
578 .apply_incoming_diff(&incoming_delete_diff(event))
579 .unwrap();
580
581 assert!(!connection.local().state().synced_event_ids().contains(&id));
582 }
583
584 #[tokio::test]
585 async fn apply_outgoing_diff_sends_create_event_for_outgoing_create() {
586 let (_tmp, mock, mut connection) = writable_connection();
587 let event = test_event();
588 connection.local().create_event(event.clone()).unwrap();
589
590 mock.reply::<rpc::CreateEvent>(event.clone());
591 connection
592 .apply_outgoing_diff(&outgoing_create_diff(event.clone()))
593 .await
594 .unwrap();
595
596 assert_eq!(mock.captured_request::<rpc::CreateEvent>().event, event);
597 }
598
599 #[tokio::test]
600 async fn apply_outgoing_diff_sends_update_event_for_outgoing_update() {
601 let (_tmp, mock, mut connection) = writable_connection();
602 let from = test_event();
603 let mut to = from.clone();
604 to.summary = Some("Updated".into());
605 connection.local().create_event(to.clone()).unwrap();
606
607 mock.reply::<rpc::UpdateEvent>(to.clone());
608 connection
609 .apply_outgoing_diff(&outgoing_update_diff(from, to.clone()))
610 .await
611 .unwrap();
612
613 assert_eq!(mock.captured_request::<rpc::UpdateEvent>().event, to);
614 }
615
616 #[tokio::test]
617 async fn apply_outgoing_diff_sends_delete_event_for_outgoing_delete() {
618 let (_tmp, mock, mut connection) = writable_connection();
619 let event = test_event();
620
621 mock.reply::<rpc::DeleteEvent>(());
622 connection
623 .apply_outgoing_diff(&outgoing_delete_diff(event.clone()))
624 .await
625 .unwrap();
626
627 assert_eq!(mock.captured_request::<rpc::DeleteEvent>().event, event);
628 }
629
630 #[tokio::test]
631 async fn apply_outgoing_diff_rewrites_local_with_canonical_event_from_provider() {
632 let (_tmp, mock, mut connection) = writable_connection();
633 let local = test_event();
634 connection.local().create_event(local.clone()).unwrap();
635
636 let mut canonical = local.clone();
637 canonical.x_properties.push(XProperty::new(
638 "X-GOOGLE-EVENT-ID".to_string(),
639 "abc123".to_string(),
640 ));
641 mock.reply::<rpc::CreateEvent>(canonical.clone());
642
643 connection
644 .apply_outgoing_diff(&outgoing_create_diff(local))
645 .await
646 .unwrap();
647
648 let reloaded = connection.local().events().unwrap();
649 assert_eq!(reloaded.len(), 1);
650 assert_eq!(reloaded[0].event().x_properties, canonical.x_properties);
651 assert_eq!(
652 connection
653 .local()
654 .state()
655 .sync_base(&canonical.event_instance_id()),
656 Some(&canonical)
657 );
658 }
659
660 #[tokio::test]
661 async fn apply_outgoing_diff_rewrites_local_when_provider_reassigns_uid() {
662 let (_tmp, mock, mut connection) = writable_connection();
667 let local = test_event();
668 let original_id = local.event_instance_id();
669 connection.local().create_event(local.clone()).unwrap();
670
671 let mut canonical = local.clone();
672 canonical.uid = EventUid::new("provider-assigned-uid@example.com");
673 let canonical_id = canonical.event_instance_id();
674 assert_ne!(original_id, canonical_id);
675 mock.reply::<rpc::CreateEvent>(canonical.clone());
676
677 connection
678 .apply_outgoing_diff(&outgoing_create_diff(local))
679 .await
680 .unwrap();
681
682 let reloaded = connection.local().events().unwrap();
683 assert_eq!(reloaded.len(), 1);
684 assert_eq!(reloaded[0].event().uid, canonical.uid);
685
686 let synced = connection.local().state().synced_event_ids();
687 assert!(synced.contains(&canonical_id));
688 assert!(!synced.contains(&original_id));
689 }
690
691 #[tokio::test]
692 async fn apply_outgoing_diff_records_base_for_outgoing_create() {
693 let (_tmp, mock, mut connection) = writable_connection();
694 let event = test_event();
695 let id = event.event_instance_id();
696 connection.local().create_event(event.clone()).unwrap();
697
698 mock.reply::<rpc::CreateEvent>(event.clone());
699 connection
700 .apply_outgoing_diff(&outgoing_create_diff(event.clone()))
701 .await
702 .unwrap();
703
704 assert_eq!(connection.local().state().sync_base(&id), Some(&event));
705 }
706
707 #[tokio::test]
708 async fn apply_outgoing_diff_persists_synced_ids_on_partial_success() {
709 use crate::provider::transport::ProviderTransportError;
710 use std::time::Duration;
711
712 let (_tmp, mock, mut connection) = writable_connection();
713
714 let event_a = test_event();
715 let event_b = test_event();
716 let id_a = event_a.event_instance_id();
717 connection.local().create_event(event_a.clone()).unwrap();
718 connection.local().create_event(event_b.clone()).unwrap();
719
720 mock.reply::<rpc::CreateEvent>(event_a.clone());
723 mock.reply_error(ProviderTransportError::Timeout(Duration::from_secs(1)));
724
725 let diff = CalendarDiff::from_changes(
726 vec![
727 EventChange::Create(event_a.clone()),
728 EventChange::Create(event_b),
729 ],
730 vec![],
731 );
732
733 let result = connection.apply_outgoing_diff(&diff).await;
734
735 assert!(
736 result.is_err(),
737 "expected the second create to propagate an error",
738 );
739
740 let reloaded = Calendar::load(connection.local().path()).unwrap();
741
742 assert_eq!(
743 reloaded.state().sync_base(&id_a),
744 Some(&event_a),
745 "event A's base should survive a later push failure",
746 );
747 }
748
749 #[tokio::test]
750 async fn discard_outgoing_diff_deletes_file_for_outgoing_create() {
751 let (_tmp, _mock, connection) = writable_connection();
752 let event = test_event();
753 let cal_event = connection.local().create_event(event.clone()).unwrap();
754 let path = cal_event.path().to_path_buf();
755
756 connection
757 .discard_outgoing_diff(&outgoing_create_diff(event))
758 .unwrap();
759
760 assert!(!path.exists());
761 }
762
763 #[tokio::test]
764 async fn discard_outgoing_diff_reverts_local_update_to_remote_version() {
765 let (_tmp, _mock, connection) = writable_connection();
766 let original = test_event();
767 let mut modified = original.clone();
768 modified.summary = Some("Locally Edited".to_string());
769 connection.local().create_event(modified.clone()).unwrap();
770
771 connection
772 .discard_outgoing_diff(&outgoing_update_diff(original.clone(), modified))
773 .unwrap();
774
775 let reloaded = connection.local().events().unwrap();
776 assert_eq!(reloaded.len(), 1);
777 assert_eq!(reloaded[0].event().summary, original.summary);
778 }
779
780 #[tokio::test]
781 async fn discard_outgoing_diff_recreates_file_for_outgoing_delete() {
782 let (_tmp, _mock, connection) = writable_connection();
783 let event = test_event();
784
785 connection
786 .discard_outgoing_diff(&outgoing_delete_diff(event))
787 .unwrap();
788
789 let expected_path = connection
790 .local()
791 .path()
792 .join("2026-01-01T1200__test-event.ics");
793 assert!(expected_path.is_file());
794 }
795}