diff --git a/tests/caldav.rs b/tests/caldav.rs index 68bdb1a..49ecc63 100644 --- a/tests/caldav.rs +++ b/tests/caldav.rs @@ -23,6 +23,12 @@ const MEETING: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\n\ BEGIN:VALARM\r\nACTION:DISPLAY\r\nTRIGGER:-PT15M\r\nDESCRIPTION:Soon\r\n\ END:VALARM\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; +/// An event written straight into the aggregate, belonging to no source. +const HAND_WRITTEN: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//phone//EN\r\n\ + BEGIN:VEVENT\r\nUID:hand-written@phone\r\nDTSTAMP:20260101T000000Z\r\n\ + DTSTART:20260911T140000Z\r\nDTEND:20260911T150000Z\r\nSUMMARY:Dentist\r\n\ + END:VEVENT\r\nEND:VCALENDAR\r\n"; + const HOLIDAY_FEED: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//feed//EN\r\n\ BEGIN:VEVENT\r\nUID:newyear@feed\r\nDTSTAMP:20260101T000000Z\r\n\ DTSTART;VALUE=DATE:20260101\r\nDTEND;VALUE=DATE:20260102\r\n\ @@ -272,3 +278,54 @@ fn a_retired_endpoints_mirror_is_reported_and_then_removed() { assert!(removed.succeeded(), "{}", removed.stderr); assert!(!mirror.exists(), "the orphaned mirror should be gone"); } + +/// An aggregate with no `default_sink` leaves events created in it alone. +/// +/// This is how a target calendar keeps events of its own: with nowhere +/// configured to file a new event, calcalist refuses to guess rather than +/// picking a source, so the event simply stays where it was written. Worth +/// pinning, because "left alone" has to hold on every subsequent cycle too — +/// an event that survived the first one and was swept up by the second would +/// be worse than never having worked. +#[test] +fn an_event_created_in_a_sinkless_aggregate_stays_where_it_is() { + let Some(fixture) = fixture() else { return }; + + // The fixture's aggregate has a default_sink; take it away. + let sinkless = std::fs::read_to_string(&fixture.calcalist.config) + .expect("read config") + .replace("default_sink = \"work\"\n", ""); + std::fs::write(&fixture.calcalist.config, sinkless).expect("rewrite config"); + + assert!(fixture.calcalist.run(&["sync"]).succeeded()); + + // Now add an event by hand, as a calendar app would. + fixture + .server + .put_event("unified", "dentist.ics", HAND_WRITTEN); + + for cycle in 1..=3 { + let run = fixture.calcalist.run(&["sync"]); + assert!( + run.succeeded(), + "cycle {cycle} failed\n{}\n{}", + run.stdout, + run.stderr + ); + + let published = fixture.server.stored("unified"); + assert!( + published.iter().any(|item| item.contains("Dentist")), + "cycle {cycle}: the hand-written event was removed:\n{published:?}" + ); + // It never reaches a source: there is nowhere it was told to go. + assert!( + !fixture + .server + .stored("work") + .iter() + .any(|item| item.contains("Dentist")), + "cycle {cycle}: the hand-written event leaked into the source" + ); + } +}