feat: RECURRENCE-ID parsing, serialization, and round-trip support
Add recurrence_id field to Event domain model. Parse and serialize RECURRENCE-ID from/to iCalendar files. Override events (with RECURRENCE-ID but no RRULE) are safe to rewrite. Save validation rejects events with both RECURRENCE-ID and RRULE.
This commit is contained in:
@@ -705,13 +705,46 @@ void test_daily_with_byday_is_supported(const std::filesystem::path& root) {
|
||||
require(rule.by_weekday.size() == 3, "three BYDAY entries expected");
|
||||
}
|
||||
|
||||
void test_recurrence_id_round_trip(const std::filesystem::path& root) {
|
||||
const auto path = root / "recurrence-id.ics";
|
||||
write_fixture(path,
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"
|
||||
"BEGIN:VEVENT\r\nUID:series-1@example.test\r\n"
|
||||
"DTSTART:20260701T090000Z\r\nDTEND:20260701T100000Z\r\n"
|
||||
"RRULE:FREQ=DAILY;COUNT=5\r\n"
|
||||
"SUMMARY:Series\r\nEND:VEVENT\r\n"
|
||||
"BEGIN:VEVENT\r\nUID:series-1@example.test\r\n"
|
||||
"RECURRENCE-ID:20260703T090000Z\r\n"
|
||||
"DTSTART:20260703T110000Z\r\nDTEND:20260703T120000Z\r\n"
|
||||
"SUMMARY:Moved occurrence\r\nEND:VEVENT\r\n"
|
||||
"END:VCALENDAR\r\n");
|
||||
|
||||
const auto loaded = nocal::storage::IcsStore::load(path);
|
||||
require(loaded.safe_to_rewrite && loaded.warnings.empty(),
|
||||
"RECURRENCE-ID should be rewrite-safe");
|
||||
require(loaded.events.size() == 2, "should parse series and override");
|
||||
require(loaded.events[0].recurrence.has_value(), "series should have RRULE");
|
||||
require(loaded.events[1].recurrence_id.has_value(), "override should have recurrence_id");
|
||||
require(!loaded.events[1].recurrence.has_value(), "override should not have RRULE");
|
||||
|
||||
const auto round_trip = root / "recurrence-id-round-trip.ics";
|
||||
nocal::storage::IcsStore::save(round_trip, loaded.events);
|
||||
const std::string serialized = read_fixture(round_trip);
|
||||
require(serialized.find("RECURRENCE-ID:20260703T090000Z") != std::string::npos,
|
||||
"RECURRENCE-ID should be serialized");
|
||||
const auto reloaded = nocal::storage::IcsStore::load(round_trip);
|
||||
require(reloaded.safe_to_rewrite && reloaded.events.size() == 2,
|
||||
"RECURRENCE-ID round-trip should be rewrite-safe");
|
||||
require(reloaded.events[1].recurrence_id.has_value(),
|
||||
"round-trip override should retain recurrence_id");
|
||||
}
|
||||
|
||||
void test_unsupported_recurrence_and_zones_are_unsafe(const std::filesystem::path& root) {
|
||||
const std::vector<std::string> unsupported_properties{
|
||||
"RRULE:FREQ=DAILY;COUNT=2;UNTIL=20260720T090000Z",
|
||||
"RRULE:FREQ=HOURLY",
|
||||
"RRULE:FREQ=DAILY;WKST=MO",
|
||||
"RDATE;VALUE=PERIOD:20260718T090000Z/PT1H",
|
||||
"RECURRENCE-ID:20260718T090000Z"};
|
||||
"RDATE;VALUE=PERIOD:20260718T090000Z/PT1H"};
|
||||
for (std::size_t index = 0; index < unsupported_properties.size(); ++index) {
|
||||
const auto path = root / ("unsupported-rule-" + std::to_string(index) + ".ics");
|
||||
write_fixture(path,
|
||||
@@ -1026,6 +1059,7 @@ int main() {
|
||||
test_unsupported_recurrence_and_zones_are_unsafe(root);
|
||||
test_ordinal_byday_round_trip(root);
|
||||
test_daily_with_byday_is_supported(root);
|
||||
test_recurrence_id_round_trip(root);
|
||||
test_save_validation_preserves_existing_bytes(root);
|
||||
test_vtimezone_custom_tzid_safe_and_round_trips(root);
|
||||
test_vtimezone_multiple_tzids(root);
|
||||
|
||||
Reference in New Issue
Block a user