diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-03 07:39:50 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-03 07:39:50 -0600 |
| commit | 84a02097bf842e96f7f4dd4e4ac39e78faf64989 (patch) | |
| tree | a3a87b6b3d75ae4179924a62c318b05fa0bd0751 /tests/test-integration-calendar-sync-recurrence-exceptions.el | |
| parent | 089a4313660cb8af1eca3829ffbdbae70f72333a (diff) | |
feat(calendar-sync): add RECURRENCE-ID exception handling for recurring events
Handle rescheduled instances of recurring calendar events by processing
RECURRENCE-ID properties from ICS files. When someone reschedules a single
instance of a recurring meeting in Google Calendar, the calendar-sync module
now shows the rescheduled time instead of the original RRULE time.
New functions:
- calendar-sync--get-recurrence-id: Extract RECURRENCE-ID from event
- calendar-sync--get-recurrence-id-line: Get full line with TZID params
- calendar-sync--parse-recurrence-id: Parse into (year month day hour minute)
- calendar-sync--collect-recurrence-exceptions: Collect all exceptions by UID
- calendar-sync--occurrence-matches-exception-p: Match occurrences to exceptions
- calendar-sync--apply-single-exception: Apply exception data to occurrence
- calendar-sync--apply-recurrence-exceptions: Apply all exceptions to occurrences
Also adds DeepSat calendar configuration (dcal-file) to user-constants,
init.el, and org-agenda-config.
48 unit and integration tests added covering normal, boundary, and error cases.
Diffstat (limited to 'tests/test-integration-calendar-sync-recurrence-exceptions.el')
| -rw-r--r-- | tests/test-integration-calendar-sync-recurrence-exceptions.el | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/tests/test-integration-calendar-sync-recurrence-exceptions.el b/tests/test-integration-calendar-sync-recurrence-exceptions.el new file mode 100644 index 00000000..0a9b5af1 --- /dev/null +++ b/tests/test-integration-calendar-sync-recurrence-exceptions.el @@ -0,0 +1,166 @@ +;;; test-integration-calendar-sync-recurrence-exceptions.el --- Integration tests -*- lexical-binding: t; -*- + +;;; Commentary: +;; Integration tests for RECURRENCE-ID exception handling workflow. +;; Tests the complete pipeline: ICS parsing → RRULE expansion → exception application. +;; Tests verify the final org output contains correct relative times. + +;;; Code: + +(require 'ert) +(add-to-list 'load-path (expand-file-name "." (file-name-directory load-file-name))) +(add-to-list 'load-path (expand-file-name "../modules" (file-name-directory load-file-name))) +(require 'testutil-calendar-sync) +(require 'calendar-sync) + +;;; Test Data - Using local times (no Z suffix) to avoid timezone conversion + +(defun test-integration-make-recurring-event-with-exception-local () + "Create ICS content with local times (no Z suffix). +Weekly meeting with one instance rescheduled from 09:00 to 10:00." + (let* ((base-start (test-calendar-sync-time-days-from-now 0 9 0)) + (base-end (test-calendar-sync-time-days-from-now 0 10 0)) + ;; Exception: week 2 moved from 9:00 to 10:00 + (exception-start (test-calendar-sync-time-days-from-now 7 10 0)) + (exception-end (test-calendar-sync-time-days-from-now 7 11 0)) + (recurrence-id-date (test-calendar-sync-time-days-from-now 7 9 0))) + (concat "BEGIN:VCALENDAR\n" + "VERSION:2.0\n" + "PRODID:-//Test//Test//EN\n" + ;; Base recurring event - use local time (no Z) + "BEGIN:VEVENT\n" + "UID:weekly-meeting@google.com\n" + "SUMMARY:Weekly Team Sync\n" + "DTSTART:" (test-calendar-sync-ics-datetime-local base-start) "\n" + "DTEND:" (test-calendar-sync-ics-datetime-local base-end) "\n" + "RRULE:FREQ=WEEKLY;COUNT=4\n" + "END:VEVENT\n" + ;; Exception event - local time + "BEGIN:VEVENT\n" + "UID:weekly-meeting@google.com\n" + "RECURRENCE-ID:" (test-calendar-sync-ics-datetime-local recurrence-id-date) "\n" + "SUMMARY:Weekly Team Sync (Rescheduled)\n" + "DTSTART:" (test-calendar-sync-ics-datetime-local exception-start) "\n" + "DTEND:" (test-calendar-sync-ics-datetime-local exception-end) "\n" + "END:VEVENT\n" + "END:VCALENDAR"))) + +;;; Integration Tests + +(ert-deftest test-integration-recurrence-exception-org-output () + "Test that parse-ics returns valid org content with exception summary." + (let* ((ics-content (test-integration-make-recurring-event-with-exception-local)) + (org-output (calendar-sync--parse-ics ics-content))) + ;; Should return string with org content + (should (stringp org-output)) + (should (string-match-p "Weekly Team Sync" org-output)) + ;; Should contain the rescheduled event with correct summary + (should (string-match-p "Rescheduled" org-output)))) + +(ert-deftest test-integration-recurrence-exception-times-in-output () + "Test that rescheduled event shows different time than regular occurrences." + (let* ((ics-content (test-integration-make-recurring-event-with-exception-local)) + (org-output (calendar-sync--parse-ics ics-content))) + ;; The org output should contain both 09:00 (regular) and 10:00 (rescheduled) + (should (string-match-p "09:00" org-output)) + (should (string-match-p "10:00" org-output)))) + +(ert-deftest test-integration-recurrence-no-exceptions-org-output () + "Test recurring event without exceptions produces correct org output." + (let* ((base-start (test-calendar-sync-time-days-from-now 0 9 0)) + (base-end (test-calendar-sync-time-days-from-now 0 10 0)) + (ics-content (concat "BEGIN:VCALENDAR\n" + "VERSION:2.0\n" + "BEGIN:VEVENT\n" + "UID:simple-weekly@google.com\n" + "SUMMARY:Simple Weekly\n" + "DTSTART:" (test-calendar-sync-ics-datetime-local base-start) "\n" + "DTEND:" (test-calendar-sync-ics-datetime-local base-end) "\n" + "RRULE:FREQ=WEEKLY;COUNT=3\n" + "END:VEVENT\n" + "END:VCALENDAR")) + (org-output (calendar-sync--parse-ics ics-content))) + ;; Should have org content + (should (stringp org-output)) + ;; Should have the event title + (should (string-match-p "Simple Weekly" org-output)) + ;; All occurrences should be at 09:00 (no exceptions) + (should (string-match-p "09:00-10:00" org-output)))) + +(ert-deftest test-integration-collect-exceptions-from-ics () + "Test that collect-recurrence-exceptions correctly extracts exceptions." + (let* ((ics-content (test-integration-make-recurring-event-with-exception-local)) + (exceptions (calendar-sync--collect-recurrence-exceptions ics-content))) + ;; Should have collected one exception + (should (hash-table-p exceptions)) + (should (gethash "weekly-meeting@google.com" exceptions)) + ;; The exception should exist + (let ((exc-list (gethash "weekly-meeting@google.com" exceptions))) + (should (= 1 (length exc-list))) + ;; The exception should have the rescheduled time (10:00) + (let* ((exc (car exc-list)) + (exc-start (plist-get exc :start))) + (should (= 10 (nth 3 exc-start))))))) + +(ert-deftest test-integration-multiple-events-with-exceptions () + "Test multiple recurring events each with their own exceptions." + (let* ((start-a (test-calendar-sync-time-days-from-now 0 9 0)) + (end-a (test-calendar-sync-time-days-from-now 0 10 0)) + (start-b (test-calendar-sync-time-days-from-now 0 14 0)) + (end-b (test-calendar-sync-time-days-from-now 0 15 0)) + ;; Exceptions for week 2: A moves from 9:00→10:00, B moves from 14:00→15:00 + (exc-a-start (test-calendar-sync-time-days-from-now 7 10 0)) + (exc-a-end (test-calendar-sync-time-days-from-now 7 11 0)) + (exc-b-start (test-calendar-sync-time-days-from-now 7 15 0)) + (exc-b-end (test-calendar-sync-time-days-from-now 7 16 0)) + (rec-id-a (test-calendar-sync-time-days-from-now 7 9 0)) + (rec-id-b (test-calendar-sync-time-days-from-now 7 14 0)) + (ics-content + (concat "BEGIN:VCALENDAR\n" + "VERSION:2.0\n" + ;; Event A base + "BEGIN:VEVENT\n" + "UID:event-a@google.com\n" + "SUMMARY:Morning Standup\n" + "DTSTART:" (test-calendar-sync-ics-datetime-local start-a) "\n" + "DTEND:" (test-calendar-sync-ics-datetime-local end-a) "\n" + "RRULE:FREQ=WEEKLY;COUNT=2\n" + "END:VEVENT\n" + ;; Event A exception + "BEGIN:VEVENT\n" + "UID:event-a@google.com\n" + "RECURRENCE-ID:" (test-calendar-sync-ics-datetime-local rec-id-a) "\n" + "SUMMARY:Morning Standup (Moved)\n" + "DTSTART:" (test-calendar-sync-ics-datetime-local exc-a-start) "\n" + "DTEND:" (test-calendar-sync-ics-datetime-local exc-a-end) "\n" + "END:VEVENT\n" + ;; Event B base + "BEGIN:VEVENT\n" + "UID:event-b@google.com\n" + "SUMMARY:Afternoon Review\n" + "DTSTART:" (test-calendar-sync-ics-datetime-local start-b) "\n" + "DTEND:" (test-calendar-sync-ics-datetime-local end-b) "\n" + "RRULE:FREQ=WEEKLY;COUNT=2\n" + "END:VEVENT\n" + ;; Event B exception + "BEGIN:VEVENT\n" + "UID:event-b@google.com\n" + "RECURRENCE-ID:" (test-calendar-sync-ics-datetime-local rec-id-b) "\n" + "SUMMARY:Afternoon Review (Moved)\n" + "DTSTART:" (test-calendar-sync-ics-datetime-local exc-b-start) "\n" + "DTEND:" (test-calendar-sync-ics-datetime-local exc-b-end) "\n" + "END:VEVENT\n" + "END:VCALENDAR")) + (org-output (calendar-sync--parse-ics ics-content))) + (should (stringp org-output)) + ;; Should have both events + (should (string-match-p "Morning Standup" org-output)) + (should (string-match-p "Afternoon Review" org-output)) + ;; Should have the "(Moved)" versions + (should (string-match-p "Moved" org-output)) + ;; Should have rescheduled times (10:00 and 15:00) + (should (string-match-p "10:00" org-output)) + (should (string-match-p "15:00" org-output)))) + +(provide 'test-integration-calendar-sync-recurrence-exceptions) +;;; test-integration-calendar-sync-recurrence-exceptions.el ends here |
