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-calendar-sync--collect-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-calendar-sync--collect-recurrence-exceptions.el')
| -rw-r--r-- | tests/test-calendar-sync--collect-recurrence-exceptions.el | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/tests/test-calendar-sync--collect-recurrence-exceptions.el b/tests/test-calendar-sync--collect-recurrence-exceptions.el new file mode 100644 index 00000000..d2567b3a --- /dev/null +++ b/tests/test-calendar-sync--collect-recurrence-exceptions.el @@ -0,0 +1,174 @@ +;;; test-calendar-sync--collect-recurrence-exceptions.el --- Tests for exception collection -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for calendar-sync--collect-recurrence-exceptions function. +;; Tests collecting all RECURRENCE-ID events from ICS content. +;; Following quality-engineer.org guidelines: one function per file. + +;;; 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 Helpers + +(defun test-make-exception-event (uid recurrence-id summary start end) + "Create a VEVENT with RECURRENCE-ID for testing. +UID is the base event UID. RECURRENCE-ID is the original occurrence date. +SUMMARY, START, END define the exception event." + (concat "BEGIN:VEVENT\n" + "UID:" uid "\n" + "RECURRENCE-ID:" recurrence-id "\n" + "SUMMARY:" summary "\n" + "DTSTART:" (test-calendar-sync-ics-datetime start) "\n" + "DTEND:" (test-calendar-sync-ics-datetime end) "\n" + "END:VEVENT")) + +(defun test-make-base-event-with-rrule (uid summary start end rrule) + "Create a recurring VEVENT with RRULE for testing." + (concat "BEGIN:VEVENT\n" + "UID:" uid "\n" + "SUMMARY:" summary "\n" + "DTSTART:" (test-calendar-sync-ics-datetime start) "\n" + "DTEND:" (test-calendar-sync-ics-datetime end) "\n" + "RRULE:" rrule "\n" + "END:VEVENT")) + +;;; Normal Cases + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-normal-single-exception () + "Test collecting single exception event." + (let* ((start (test-calendar-sync-time-days-from-now 7 10 0)) + (end (test-calendar-sync-time-days-from-now 7 11 0)) + (ics (test-calendar-sync-make-ics + (test-make-exception-event "event123@google.com" + "20260203T090000Z" + "Rescheduled Meeting" + start end))) + (exceptions (calendar-sync--collect-recurrence-exceptions ics))) + ;; Should have one exception keyed by UID + (should (hash-table-p exceptions)) + (should (gethash "event123@google.com" exceptions)))) + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-normal-multiple-same-uid () + "Test collecting multiple exceptions for same recurring event." + (let* ((start1 (test-calendar-sync-time-days-from-now 7 10 0)) + (end1 (test-calendar-sync-time-days-from-now 7 11 0)) + (start2 (test-calendar-sync-time-days-from-now 14 11 0)) + (end2 (test-calendar-sync-time-days-from-now 14 12 0)) + (ics (test-calendar-sync-make-ics + (test-make-exception-event "weekly123@google.com" + "20260203T090000Z" + "Week 1 Rescheduled" + start1 end1) + (test-make-exception-event "weekly123@google.com" + "20260210T090000Z" + "Week 2 Rescheduled" + start2 end2))) + (exceptions (calendar-sync--collect-recurrence-exceptions ics))) + ;; Should have list of exceptions under single UID + (should (hash-table-p exceptions)) + (let ((uid-exceptions (gethash "weekly123@google.com" exceptions))) + (should uid-exceptions) + (should (= 2 (length uid-exceptions)))))) + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-normal-multiple-uids () + "Test collecting exceptions for different recurring events." + (let* ((start (test-calendar-sync-time-days-from-now 7 10 0)) + (end (test-calendar-sync-time-days-from-now 7 11 0)) + (ics (test-calendar-sync-make-ics + (test-make-exception-event "event-a@google.com" + "20260203T090000Z" + "Event A Rescheduled" + start end) + (test-make-exception-event "event-b@google.com" + "20260203T140000Z" + "Event B Rescheduled" + start end))) + (exceptions (calendar-sync--collect-recurrence-exceptions ics))) + ;; Should have two UIDs + (should (hash-table-p exceptions)) + (should (gethash "event-a@google.com" exceptions)) + (should (gethash "event-b@google.com" exceptions)))) + +;;; Boundary Cases + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-boundary-no-exceptions () + "Test ICS with no RECURRENCE-ID events returns empty hash." + (let* ((start (test-calendar-sync-time-days-from-now 7 10 0)) + (end (test-calendar-sync-time-days-from-now 7 11 0)) + (ics (test-calendar-sync-make-ics + (test-calendar-sync-make-vevent "Regular Event" start end))) + (exceptions (calendar-sync--collect-recurrence-exceptions ics))) + (should (hash-table-p exceptions)) + (should (= 0 (hash-table-count exceptions))))) + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-boundary-mixed-events () + "Test ICS with both regular and exception events." + (let* ((start (test-calendar-sync-time-days-from-now 7 10 0)) + (end (test-calendar-sync-time-days-from-now 7 11 0)) + (base-start (test-calendar-sync-time-days-from-now 1 9 0)) + (base-end (test-calendar-sync-time-days-from-now 1 10 0)) + (ics (test-calendar-sync-make-ics + ;; Regular event (no RECURRENCE-ID) + (test-calendar-sync-make-vevent "Normal Meeting" base-start base-end) + ;; Base recurring event with RRULE + (test-make-base-event-with-rrule "recurring@google.com" + "Weekly Sync" + base-start base-end + "FREQ=WEEKLY;COUNT=10") + ;; Exception to the recurring event + (test-make-exception-event "recurring@google.com" + "20260210T090000Z" + "Weekly Sync (Rescheduled)" + start end))) + (exceptions (calendar-sync--collect-recurrence-exceptions ics))) + ;; Should only collect the exception, not regular or base events + (should (hash-table-p exceptions)) + (should (= 1 (hash-table-count exceptions))) + (should (gethash "recurring@google.com" exceptions)))) + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-boundary-tzid-exception () + "Test exception event with TZID-qualified RECURRENCE-ID." + (let* ((start (test-calendar-sync-time-days-from-now 7 10 0)) + (end (test-calendar-sync-time-days-from-now 7 11 0)) + (event (concat "BEGIN:VEVENT\n" + "UID:tzid-event@google.com\n" + "RECURRENCE-ID;TZID=Europe/Tallinn:20260203T170000\n" + "SUMMARY:Tallinn Meeting Rescheduled\n" + "DTSTART:" (test-calendar-sync-ics-datetime start) "\n" + "DTEND:" (test-calendar-sync-ics-datetime end) "\n" + "END:VEVENT")) + (ics (test-calendar-sync-make-ics event)) + (exceptions (calendar-sync--collect-recurrence-exceptions ics))) + (should (hash-table-p exceptions)) + (should (gethash "tzid-event@google.com" exceptions)))) + +;;; Error Cases + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-error-empty-string () + "Test empty ICS content returns empty hash." + (let ((exceptions (calendar-sync--collect-recurrence-exceptions ""))) + (should (hash-table-p exceptions)) + (should (= 0 (hash-table-count exceptions))))) + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-error-nil-input () + "Test nil input returns empty hash or nil." + (let ((exceptions (calendar-sync--collect-recurrence-exceptions nil))) + (should (or (null exceptions) + (and (hash-table-p exceptions) + (= 0 (hash-table-count exceptions))))))) + +(ert-deftest test-calendar-sync--collect-recurrence-exceptions-error-malformed-ics () + "Test malformed ICS handles gracefully." + (let ((exceptions (calendar-sync--collect-recurrence-exceptions "not valid ics content"))) + ;; Should not crash, return empty hash + (should (or (null exceptions) + (and (hash-table-p exceptions) + (= 0 (hash-table-count exceptions))))))) + +(provide 'test-calendar-sync--collect-recurrence-exceptions) +;;; test-calendar-sync--collect-recurrence-exceptions.el ends here |
