From b1112822e7eb7f4788ba21f8939d804b10084510 Mon Sep 17 00:00:00 2001 From: Wjouter <34310151+Wjouter@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:54:56 +0100 Subject: [PATCH] test(checker): Google-shaped feed records time moves and fans them out A report of missed time changes on a Google calendar turned out to be Google serving a stale feed, but no fixture had Google's shape: all-day VALUE=DATE events, UTC times and a "this event only" RECURRENCE-ID override. The new test moves one of each and asserts a changed/time row per event and a pending delivery per change. Co-Authored-By: Claude Opus 5.5 --- backend/InTheKnow.Tests/CheckerTests.cs | 23 ++++++ .../Fixtures/google-edited.ics | 76 +++++++++++++++++++ backend/InTheKnow.Tests/Fixtures/google.ics | 63 +++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 backend/InTheKnow.Tests/Fixtures/google-edited.ics create mode 100644 backend/InTheKnow.Tests/Fixtures/google.ics diff --git a/backend/InTheKnow.Tests/CheckerTests.cs b/backend/InTheKnow.Tests/CheckerTests.cs index 466e2c2..5c9ba68 100644 --- a/backend/InTheKnow.Tests/CheckerTests.cs +++ b/backend/InTheKnow.Tests/CheckerTests.cs @@ -206,6 +206,29 @@ public async Task GenericFeed() Assert.True(await h.Db.ScalarAsync("SELECT count(*) FROM event_snapshot WHERE calendar_id = $1", id) > before); } + // Google Calendar's shape: X-WR-TIMEZONE, all-day VALUE=DATE, UTC times, and a moved occurrence as a RECURRENCE-ID override. + [Fact] + public async Task GoogleFeedTimeMovesAreRecordedAndFannedOut() + { + var (h, id, url) = await BaselineAsync("google.ics", "calendar.google.com"); + await using var _h = h; + Assert.Equal(6, await h.Db.ScalarAsync("SELECT count(*) FROM event_snapshot WHERE calendar_id = $1", id)); + var channel = await ChannelAsync(h.Db, h.S1); + + h.Http.Map[url] = "google-edited.ics"; + await NextIntervalAsync(h); + Assert.Equal( + [ + ("allday@google.com", "changed", "time"), + ("single@google.com", "changed", "time"), + ("weekly@google.com|2026-11-09T08:00:00Z", "changed", "time"), + ], Sorted(await h.ChangesAsync(id))); + var single = await h.Db.RowsAsync("SELECT old_value::text, new_value::text FROM detected_change WHERE event_key = 'single@google.com'"); + Assert.Equal([["""{"end": "2026-11-12T16:30:00Z", "start": "2026-11-12T16:00:00Z"}""", + """{"end": "2026-11-12T17:30:00Z", "start": "2026-11-12T17:00:00Z"}"""]], single); + Assert.Equal([[channel, 3L]], await h.Db.RowsAsync("SELECT channel_id, count(*) FROM delivery WHERE status = 'pending' GROUP BY channel_id")); + } + [Fact] public async Task UnchangedFeedWritesNoSnapshotRows() { diff --git a/backend/InTheKnow.Tests/Fixtures/google-edited.ics b/backend/InTheKnow.Tests/Fixtures/google-edited.ics new file mode 100644 index 0000000..3600989 --- /dev/null +++ b/backend/InTheKnow.Tests/Fixtures/google-edited.ics @@ -0,0 +1,76 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Synthetic Google test calendar +X-WR-TIMEZONE:Europe/Brussels +BEGIN:VTIMEZONE +TZID:Europe/Brussels +X-LIC-LOCATION:Europe/Brussels +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;VALUE=DATE:20261111 +DTEND;VALUE=DATE:20261112 +DTSTAMP:20260924T120000Z +UID:allday@google.com +CREATED:20260901T100000Z +LAST-MODIFIED:20260901T100000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:All-day event +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20261112T170000Z +DTEND:20261112T173000Z +DTSTAMP:20260924T120000Z +UID:single@google.com +CREATED:20260901T100000Z +LAST-MODIFIED:20260901T100000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Single event +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Brussels:20261102T090000 +DTEND;TZID=Europe/Brussels:20261102T100000 +DTSTAMP:20260924T120000Z +UID:weekly@google.com +RRULE:FREQ=WEEKLY;COUNT=4 +CREATED:20260901T100000Z +LAST-MODIFIED:20260901T100000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Weekly lesson +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Brussels:20261109T110000 +DTEND;TZID=Europe/Brussels:20261109T120000 +DTSTAMP:20260924T120000Z +UID:weekly@google.com +RECURRENCE-ID;TZID=Europe/Brussels:20261109T090000 +CREATED:20260901T100000Z +LAST-MODIFIED:20260901T100000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Weekly lesson +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/backend/InTheKnow.Tests/Fixtures/google.ics b/backend/InTheKnow.Tests/Fixtures/google.ics new file mode 100644 index 0000000..38c03ea --- /dev/null +++ b/backend/InTheKnow.Tests/Fixtures/google.ics @@ -0,0 +1,63 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Synthetic Google test calendar +X-WR-TIMEZONE:Europe/Brussels +BEGIN:VTIMEZONE +TZID:Europe/Brussels +X-LIC-LOCATION:Europe/Brussels +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;VALUE=DATE:20261110 +DTEND;VALUE=DATE:20261111 +DTSTAMP:20260924T120000Z +UID:allday@google.com +CREATED:20260901T100000Z +LAST-MODIFIED:20260901T100000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:All-day event +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20261112T160000Z +DTEND:20261112T163000Z +DTSTAMP:20260924T120000Z +UID:single@google.com +CREATED:20260901T100000Z +LAST-MODIFIED:20260901T100000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Single event +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Brussels:20261102T090000 +DTEND;TZID=Europe/Brussels:20261102T100000 +DTSTAMP:20260924T120000Z +UID:weekly@google.com +RRULE:FREQ=WEEKLY;COUNT=4 +CREATED:20260901T100000Z +LAST-MODIFIED:20260901T100000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Weekly lesson +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR