feat: make calendar import timezone-correct and bounded (Closes #1212)
This commit is contained in:
1 parent
7a9347bd2a
commit
69a3a2c568
4 files changed
+198
-27
No files matched your search
@@ -30,7 +30,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
try:
|
||||
with release_server(archives[0], tmp_path, f"http://127.0.0.1:{fake.server_port}") as origin, sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(args=["--ignore-certificate-errors"])
|
||||
page = browser.new_page(viewport={"width": width, "height": height})
|
||||
page = browser.new_page(viewport={"width": width, "height": height}, timezone_id="UTC")
|
||||
page_errors: list[str] = []
|
||||
page.on("pageerror", lambda error: page_errors.append(str(error)))
|
||||
|
||||
@@ -107,9 +107,10 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
added = (date.today() + timedelta(days=4)).strftime("%Y%m%d")
|
||||
calendar = (
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"
|
||||
f"SUMMARY:{private_title}\r\nDTSTART:{tomorrow}T090000\r\n"
|
||||
f"DTEND:{tomorrow}T100000\r\nRRULE:FREQ=DAILY;COUNT=3\r\n"
|
||||
f"EXDATE:{excluded}T090000\r\nRDATE:{added}T090000\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
||||
f"SUMMARY:{private_title}\r\nDTSTART;TZID=America/New_York:{tomorrow}T050000\r\n"
|
||||
f"DTEND;TZID=America/New_York:{tomorrow}T060000\r\nRRULE:FREQ=DAILY;COUNT=3\r\n"
|
||||
f"EXDATE;TZID=America/New_York:{excluded}T050000\r\n"
|
||||
f"RDATE;TZID=America/New_York:{added}T050000\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
||||
)
|
||||
unsupported_title = "Private monthly board review"
|
||||
unsupported_calendar = (
|
||||
|
||||
@@ -42,6 +42,104 @@ console.log(JSON.stringify({days,serialized:JSON.stringify(days)}));
|
||||
assert private_value not in result["serialized"]
|
||||
|
||||
|
||||
def test_calendar_import_converts_iana_tzid_into_the_device_local_workday():
|
||||
result = run_import("""
|
||||
process.env.TZ='America/Los_Angeles';
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20260821T120000\r\nDTEND;TZID=America/New_York:20260821T130000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'});
|
||||
console.log(JSON.stringify({first:days[0]}));
|
||||
""")
|
||||
|
||||
assert result["first"]["busy_minutes"] == 60
|
||||
assert result["first"]["free_windows"] == [
|
||||
{"start_time": "10:00", "end_time": "17:00"},
|
||||
]
|
||||
|
||||
|
||||
def test_calendar_import_fails_closed_for_unknown_tzid_without_exposing_event_details():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nSUMMARY:Private acquisition\r\nATTENDEE:mailto:secret@example.com\r\nDTSTART;TZID=Private/Boardroom:20260821T120000\r\nDTEND;TZID=Private/Boardroom:20260821T130000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates});
|
||||
console.log(JSON.stringify({unsupported:days.unsupported_count,busy:days.map(day=>day.busy_minutes),serialized:JSON.stringify(days)}));
|
||||
""")
|
||||
|
||||
assert result["unsupported"] == 1
|
||||
assert result["busy"] == [0] * 7
|
||||
assert "Private acquisition" not in result["serialized"]
|
||||
assert "secret@example.com" not in result["serialized"]
|
||||
|
||||
|
||||
def test_calendar_import_applies_tzid_to_rdate_and_exdate_values():
|
||||
result = run_import("""
|
||||
process.env.TZ='America/Los_Angeles';
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20260821T120000\r\nDTEND;TZID=America/New_York:20260821T130000\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEXDATE;TZID=America/New_York:20260822T120000\r\nRDATE;TZID=America/New_York:20260824T120000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates});
|
||||
console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes),fourth:days[3].free_windows}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [60, 0, 60, 60, 0, 0, 0]
|
||||
assert result["fourth"] == [{"start_time": "10:00", "end_time": "17:00"}]
|
||||
|
||||
|
||||
def test_calendar_import_keeps_zoned_daily_recurrence_at_wall_time_across_dst():
|
||||
result = run_import("""
|
||||
process.env.TZ='UTC';
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20260307T090000\r\nDTEND;TZID=America/New_York:20260307T100000\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-03-07','2026-03-08','2026-03-09','2026-03-10','2026-03-11','2026-03-12','2026-03-13'];
|
||||
const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'});
|
||||
console.log(JSON.stringify({windows:days.slice(0,3).map(day=>day.free_windows)}));
|
||||
""")
|
||||
|
||||
assert result["windows"] == [
|
||||
[{"start_time": "09:00", "end_time": "14:00"}, {"start_time": "15:00", "end_time": "17:00"}],
|
||||
[{"start_time": "09:00", "end_time": "13:00"}, {"start_time": "14:00", "end_time": "17:00"}],
|
||||
[{"start_time": "09:00", "end_time": "13:00"}, {"start_time": "14:00", "end_time": "17:00"}],
|
||||
]
|
||||
|
||||
|
||||
def test_calendar_import_interprets_floating_until_in_the_event_timezone():
|
||||
result = run_import("""
|
||||
process.env.TZ='UTC';
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20260307T090000\r\nDTEND;TZID=America/New_York:20260307T100000\r\nRRULE:FREQ=DAILY;UNTIL=20260308T090000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-03-07','2026-03-08','2026-03-09','2026-03-10','2026-03-11','2026-03-12','2026-03-13'];
|
||||
const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'});
|
||||
console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [60, 60, 0, 0, 0, 0, 0]
|
||||
|
||||
|
||||
def test_calendar_import_fast_forwards_historical_daily_series_to_seven_day_range():
|
||||
result = run_import("""
|
||||
process.env.TZ='UTC';
|
||||
const event=`BEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20000101T090000\r\nDTEND;TZID=America/New_York:20000101T100000\r\nRRULE:FREQ=DAILY\r\nEND:VEVENT\r\n`;
|
||||
const source='BEGIN:VCALENDAR\\r\\nVERSION:2.0\\r\\n'+event.repeat(80)+'END:VCALENDAR';
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const started=Date.now();const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'});
|
||||
console.log(JSON.stringify({elapsed:Date.now()-started,busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [60] * 7
|
||||
assert result["elapsed"] < 1000
|
||||
|
||||
|
||||
def test_calendar_import_fast_forwards_historical_weekly_series_to_seven_day_range():
|
||||
result = run_import("""
|
||||
process.env.TZ='UTC';
|
||||
const event=`BEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20000101T090000\r\nDTEND;TZID=America/New_York:20000101T100000\r\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2\r\nEND:VEVENT\r\n`;
|
||||
const source='BEGIN:VCALENDAR\\r\\nVERSION:2.0\\r\\n'+event.repeat(80)+'END:VCALENDAR';
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const started=Date.now();const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'});
|
||||
console.log(JSON.stringify({elapsed:Date.now()-started,busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [0, 0, 0, 60, 0, 60, 0]
|
||||
assert result["elapsed"] < 1000
|
||||
|
||||
|
||||
def test_calendar_import_expands_bounded_daily_recurrence_and_honors_exdate():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:standup\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nRRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20260823T100000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
|
||||
Reference in new issue
Block a user