feat: embedded VTIMEZONE parsing, round-trip serialization
This commit is contained in:
@@ -697,8 +697,14 @@ void test_unsupported_recurrence_and_zones_are_unsafe(const std::filesystem::pat
|
||||
"END:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:custom\r\n"
|
||||
"DTSTART:20260717T090000Z\r\nDTEND:20260717T100000Z\r\n"
|
||||
"END:VEVENT\r\nEND:VCALENDAR\r\n");
|
||||
require(!nocal::storage::IcsStore::load(custom_zone).safe_to_rewrite,
|
||||
"VTIMEZONE definition was considered rewrite-safe");
|
||||
// VTIMEZONE is now parsed and preserved for round-trip, so the calendar
|
||||
// is safe to rewrite even though the custom TZID is unused by the event.
|
||||
const auto vtz_result = nocal::storage::IcsStore::load(custom_zone);
|
||||
require(vtz_result.safe_to_rewrite,
|
||||
"VTIMEZONE definition should be safe when round-trippable");
|
||||
require(vtz_result.vtimezones.size() == 1 &&
|
||||
vtz_result.vtimezones[0].tzid == "Custom/Test",
|
||||
"VTIMEZONE should be captured even when no event references it");
|
||||
|
||||
struct InvalidRdate {
|
||||
std::string start;
|
||||
@@ -813,6 +819,131 @@ void test_save_validation_preserves_existing_bytes(const std::filesystem::path&
|
||||
require_no_temporary_files(path);
|
||||
}
|
||||
|
||||
void test_vtimezone_custom_tzid_safe_and_round_trips(const std::filesystem::path& root) {
|
||||
const auto path = root / "vtimezone-custom.ics";
|
||||
write_fixture(path,
|
||||
"BEGIN:VCALENDAR\r\n"
|
||||
"VERSION:2.0\r\n"
|
||||
"BEGIN:VTIMEZONE\r\n"
|
||||
"TZID:Custom/Example\r\n"
|
||||
"BEGIN:STANDARD\r\n"
|
||||
"TZOFFSETFROM:+0100\r\n"
|
||||
"TZOFFSETTO:+0000\r\n"
|
||||
"DTSTART:19701025T030000\r\n"
|
||||
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n"
|
||||
"END:STANDARD\r\n"
|
||||
"BEGIN:DAYLIGHT\r\n"
|
||||
"TZOFFSETFROM:+0000\r\n"
|
||||
"TZOFFSETTO:+0100\r\n"
|
||||
"DTSTART:19700329T020000\r\n"
|
||||
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\r\n"
|
||||
"END:DAYLIGHT\r\n"
|
||||
"END:VTIMEZONE\r\n"
|
||||
"BEGIN:VEVENT\r\n"
|
||||
"UID:custom-tz\r\n"
|
||||
"DTSTART;TZID=Custom/Example:20260717T100000\r\n"
|
||||
"DTEND;TZID=Custom/Example:20260717T110000\r\n"
|
||||
"SUMMARY:Custom zone event\r\n"
|
||||
"END:VEVENT\r\n"
|
||||
"END:VCALENDAR\r\n");
|
||||
|
||||
const auto loaded = nocal::storage::IcsStore::load(path);
|
||||
require(loaded.safe_to_rewrite,
|
||||
"VTIMEZONE-defined TZID should make the calendar safe to rewrite");
|
||||
require(loaded.events.size() == 1, "VTIMEZONE calendar should contain the event");
|
||||
require(loaded.events[0].time_zone == "Custom/Example",
|
||||
"VTIMEZONE-defined TZID should be retained on the event");
|
||||
require(loaded.events[0].time_basis == nocal::TimeBasis::zoned,
|
||||
"zoned time basis should be preserved");
|
||||
require(loaded.vtimezones.size() == 1,
|
||||
"VTIMEZONE definition should be captured");
|
||||
require(loaded.vtimezones[0].tzid == "Custom/Example",
|
||||
"VTIMEZONE TZID should match");
|
||||
require(!loaded.vtimezones[0].raw.empty(),
|
||||
"VTIMEZONE raw content should be captured");
|
||||
|
||||
// Verify a VTIMEZONE-DST warning was emitted for unknown TZID.
|
||||
bool has_dst_warning = false;
|
||||
for (const auto& w : loaded.warnings) {
|
||||
if (w.find("DST") != std::string::npos) {
|
||||
has_dst_warning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
require(has_dst_warning,
|
||||
"should warn that DST handling is approximate for custom VTIMEZONE");
|
||||
|
||||
// Round-trip: save with VTIMEZONE preserved, then reload.
|
||||
const auto saved_revision = nocal::storage::IcsStore::save(
|
||||
path, loaded.events, loaded.revision, loaded.vtimezones);
|
||||
require(saved_revision.existed(), "save should return an existing revision");
|
||||
|
||||
const std::string bytes = read_fixture(path);
|
||||
require(bytes.find("BEGIN:VTIMEZONE") != std::string::npos,
|
||||
"VTIMEZONE block should be serialized on save");
|
||||
require(bytes.find("TZID:Custom/Example") != std::string::npos,
|
||||
"VTIMEZONE TZID should appear in serialized output");
|
||||
require(bytes.find("BEGIN:STANDARD") != std::string::npos,
|
||||
"VTIMEZONE sub-components should be preserved");
|
||||
|
||||
const auto reloaded = nocal::storage::IcsStore::load(path);
|
||||
require(reloaded.safe_to_rewrite, "round-tripped VTIMEZONE calendar should be safe");
|
||||
require(reloaded.events.size() == 1, "round-trip event count should match");
|
||||
require(reloaded.events[0].time_zone == "Custom/Example",
|
||||
"round-trip TZID should be preserved");
|
||||
require(reloaded.vtimezones.size() == 1,
|
||||
"round-trip VTIMEZONE count should match");
|
||||
require(reloaded.vtimezones[0].tzid == "Custom/Example",
|
||||
"round-trip VTIMEZONE TZID should match");
|
||||
require_no_temporary_files(path);
|
||||
}
|
||||
|
||||
void test_vtimezone_multiple_tzids(const std::filesystem::path& root) {
|
||||
const auto path = root / "vtimezone-multiple.ics";
|
||||
write_fixture(path,
|
||||
"BEGIN:VCALENDAR\r\n"
|
||||
"VERSION:2.0\r\n"
|
||||
"BEGIN:VTIMEZONE\r\nTZID:Custom/A\r\nEND:VTIMEZONE\r\n"
|
||||
"BEGIN:VTIMEZONE\r\nTZID:Custom/B\r\nEND:VTIMEZONE\r\n"
|
||||
"BEGIN:VEVENT\r\n"
|
||||
"UID:evt-a\r\n"
|
||||
"DTSTART;TZID=Custom/A:20260717T100000\r\n"
|
||||
"DTEND;TZID=Custom/A:20260717T110000\r\n"
|
||||
"SUMMARY:Event A\r\n"
|
||||
"END:VEVENT\r\n"
|
||||
"BEGIN:VEVENT\r\n"
|
||||
"UID:evt-b\r\n"
|
||||
"DTSTART;TZID=Custom/B:20260718T100000\r\n"
|
||||
"DTEND;TZID=Custom/B:20260718T110000\r\n"
|
||||
"SUMMARY:Event B\r\n"
|
||||
"END:VEVENT\r\n"
|
||||
"END:VCALENDAR\r\n");
|
||||
|
||||
const auto loaded = nocal::storage::IcsStore::load(path);
|
||||
require(loaded.safe_to_rewrite,
|
||||
"multiple VTIMEZONE-defined TZIDs should be safe");
|
||||
require(loaded.events.size() == 2, "should parse all events");
|
||||
require(loaded.vtimezones.size() == 2, "should capture both VTIMEZONE definitions");
|
||||
}
|
||||
|
||||
void test_vtimezone_undefined_tzid_still_unsafe(const std::filesystem::path& root) {
|
||||
const auto path = root / "vtimezone-undefined.ics";
|
||||
// Event uses Custom/Unknown which is NOT defined in any VTIMEZONE.
|
||||
write_fixture(path,
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"
|
||||
"BEGIN:VTIMEZONE\r\nTZID:Custom/Defined\r\nEND:VTIMEZONE\r\n"
|
||||
"BEGIN:VEVENT\r\nUID:undef\r\n"
|
||||
"DTSTART;TZID=Custom/Unknown:20260717T100000\r\n"
|
||||
"DTEND;TZID=Custom/Unknown:20260717T110000\r\n"
|
||||
"SUMMARY:Undefined TZID\r\nEND:VEVENT\r\n"
|
||||
"END:VCALENDAR\r\n");
|
||||
|
||||
const auto loaded = nocal::storage::IcsStore::load(path);
|
||||
require(!loaded.safe_to_rewrite,
|
||||
"undefined TZID should still be unsafe even when other VTIMEZONEs exist");
|
||||
require(loaded.events.size() == 1, "event should still be browseable");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -843,6 +974,9 @@ int main() {
|
||||
test_supported_rdates(root);
|
||||
test_unsupported_recurrence_and_zones_are_unsafe(root);
|
||||
test_save_validation_preserves_existing_bytes(root);
|
||||
test_vtimezone_custom_tzid_safe_and_round_trips(root);
|
||||
test_vtimezone_multiple_tzids(root);
|
||||
test_vtimezone_undefined_tzid_still_unsafe(root);
|
||||
std::filesystem::remove_all(root, ignored);
|
||||
std::cout << "ics_tests: ok\n";
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user