summaryrefslogtreecommitdiff
path: root/tests/test-calendar-sync--apply-recurrence-exceptions.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-03 07:39:50 -0600
committerCraig Jennings <c@cjennings.net>2026-02-03 07:39:50 -0600
commit84a02097bf842e96f7f4dd4e4ac39e78faf64989 (patch)
treea3a87b6b3d75ae4179924a62c318b05fa0bd0751 /tests/test-calendar-sync--apply-recurrence-exceptions.el
parent089a4313660cb8af1eca3829ffbdbae70f72333a (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--apply-recurrence-exceptions.el')
-rw-r--r--tests/test-calendar-sync--apply-recurrence-exceptions.el157
1 files changed, 157 insertions, 0 deletions
diff --git a/tests/test-calendar-sync--apply-recurrence-exceptions.el b/tests/test-calendar-sync--apply-recurrence-exceptions.el
new file mode 100644
index 00000000..7711c5cb
--- /dev/null
+++ b/tests/test-calendar-sync--apply-recurrence-exceptions.el
@@ -0,0 +1,157 @@
+;;; test-calendar-sync--apply-recurrence-exceptions.el --- Tests for applying exceptions -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--apply-recurrence-exceptions function.
+;; Tests applying RECURRENCE-ID exceptions to expanded occurrences.
+;; 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-occurrence (year month day hour minute summary &optional uid)
+ "Create an occurrence plist for testing.
+Mirrors the format used by calendar-sync expansion functions.
+Uses :start (year month day hour minute) format."
+ (list :start (list year month day hour minute)
+ :end (list year month day (1+ hour) minute)
+ :summary summary
+ :uid (or uid "test-event@google.com")))
+
+(defun test-make-exception-data (rec-year rec-month rec-day rec-hour rec-minute
+ new-year new-month new-day new-hour new-minute
+ &optional summary)
+ "Create exception data plist for testing.
+REC-* are the original occurrence date/time (recurrence-id).
+NEW-* values are the rescheduled time."
+ (list :recurrence-id (list rec-year rec-month rec-day rec-hour rec-minute)
+ :start (list new-year new-month new-day new-hour new-minute)
+ :end (list new-year new-month new-day (1+ new-hour) new-minute)
+ :summary (or summary "Rescheduled")))
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-normal-single-replacement ()
+ "Test replacing single occurrence with exception."
+ (let* ((occurrences (list (test-make-occurrence 2026 2 3 9 0 "Weekly Meeting")))
+ (exceptions (make-hash-table :test 'equal)))
+ ;; Exception: Feb 3 at 9:00 → Feb 3 at 10:00
+ (puthash "test-event@google.com"
+ (list (test-make-exception-data 2026 2 3 9 0 2026 2 3 10 0))
+ exceptions)
+ (let ((result (calendar-sync--apply-recurrence-exceptions occurrences exceptions)))
+ (should (= 1 (length result)))
+ ;; Time should be updated to 10:00
+ (should (= 10 (nth 3 (plist-get (car result) :start)))))))
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-normal-multiple-some-replaced ()
+ "Test multiple occurrences with one exception."
+ (let* ((occurrences (list (test-make-occurrence 2026 2 3 9 0 "Weekly Meeting")
+ (test-make-occurrence 2026 2 10 9 0 "Weekly Meeting")
+ (test-make-occurrence 2026 2 17 9 0 "Weekly Meeting")))
+ (exceptions (make-hash-table :test 'equal)))
+ ;; Only Feb 10 is rescheduled
+ (puthash "test-event@google.com"
+ (list (test-make-exception-data 2026 2 10 9 0 2026 2 10 11 0))
+ exceptions)
+ (let ((result (calendar-sync--apply-recurrence-exceptions occurrences exceptions)))
+ (should (= 3 (length result)))
+ ;; Feb 3: unchanged (9:00)
+ (should (= 9 (nth 3 (plist-get (nth 0 result) :start))))
+ ;; Feb 10: rescheduled (11:00)
+ (should (= 11 (nth 3 (plist-get (nth 1 result) :start))))
+ ;; Feb 17: unchanged (9:00)
+ (should (= 9 (nth 3 (plist-get (nth 2 result) :start)))))))
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-normal-date-change ()
+ "Test exception that changes the date, not just time."
+ (let* ((occurrences (list (test-make-occurrence 2026 2 3 9 0 "Weekly Meeting")))
+ (exceptions (make-hash-table :test 'equal)))
+ ;; Feb 3 rescheduled to Feb 4
+ (puthash "test-event@google.com"
+ (list (test-make-exception-data 2026 2 3 9 0 2026 2 4 10 0))
+ exceptions)
+ (let ((result (calendar-sync--apply-recurrence-exceptions occurrences exceptions)))
+ (should (= 1 (length result)))
+ (let ((new-start (plist-get (car result) :start)))
+ (should (= 4 (nth 2 new-start))) ; day
+ (should (= 10 (nth 3 new-start))))))) ; hour
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-boundary-no-exceptions ()
+ "Test occurrences returned unchanged when no exceptions."
+ (let* ((occurrences (list (test-make-occurrence 2026 2 3 9 0 "Weekly Meeting")
+ (test-make-occurrence 2026 2 10 9 0 "Weekly Meeting")))
+ (exceptions (make-hash-table :test 'equal)))
+ (let ((result (calendar-sync--apply-recurrence-exceptions occurrences exceptions)))
+ (should (= 2 (length result)))
+ (should (= 9 (nth 3 (plist-get (nth 0 result) :start))))
+ (should (= 9 (nth 3 (plist-get (nth 1 result) :start)))))))
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-boundary-exception-no-match ()
+ "Test exception for different UID doesn't affect occurrences."
+ (let* ((occurrences (list (test-make-occurrence 2026 2 3 9 0 "Weekly Meeting" "event-a@google.com")))
+ (exceptions (make-hash-table :test 'equal)))
+ ;; Exception is for different UID
+ (puthash "event-b@google.com"
+ (list (test-make-exception-data 2026 2 3 9 0 2026 2 3 11 0))
+ exceptions)
+ (let ((result (calendar-sync--apply-recurrence-exceptions occurrences exceptions)))
+ (should (= 1 (length result)))
+ ;; Should remain at 9:00, not 11:00
+ (should (= 9 (nth 3 (plist-get (car result) :start)))))))
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-boundary-multiple-uids ()
+ "Test exceptions for multiple different recurring events."
+ (let* ((occurrences (list (test-make-occurrence 2026 2 3 9 0 "Meeting A" "event-a@google.com")
+ (test-make-occurrence 2026 2 3 14 0 "Meeting B" "event-b@google.com")))
+ (exceptions (make-hash-table :test 'equal)))
+ ;; Different exceptions for each UID
+ (puthash "event-a@google.com"
+ (list (test-make-exception-data 2026 2 3 9 0 2026 2 3 10 0))
+ exceptions)
+ (puthash "event-b@google.com"
+ (list (test-make-exception-data 2026 2 3 14 0 2026 2 3 15 0))
+ exceptions)
+ (let ((result (calendar-sync--apply-recurrence-exceptions occurrences exceptions)))
+ (should (= 2 (length result)))
+ ;; Each should have its respective exception applied
+ (should (= 10 (nth 3 (plist-get (nth 0 result) :start))))
+ (should (= 15 (nth 3 (plist-get (nth 1 result) :start)))))))
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-boundary-empty-occurrences ()
+ "Test empty occurrences list returns empty."
+ (let* ((occurrences '())
+ (exceptions (make-hash-table :test 'equal)))
+ (puthash "test@google.com"
+ (list (test-make-exception-data 2026 2 3 9 0 2026 2 3 10 0))
+ exceptions)
+ (let ((result (calendar-sync--apply-recurrence-exceptions occurrences exceptions)))
+ (should (= 0 (length result))))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-error-nil-occurrences ()
+ "Test nil occurrences handled gracefully."
+ (let ((exceptions (make-hash-table :test 'equal)))
+ (let ((result (calendar-sync--apply-recurrence-exceptions nil exceptions)))
+ (should (or (null result) (= 0 (length result)))))))
+
+(ert-deftest test-calendar-sync--apply-recurrence-exceptions-error-nil-exceptions ()
+ "Test nil exceptions handled gracefully."
+ (let ((occurrences (list (test-make-occurrence 2026 2 3 9 0 "Meeting"))))
+ (let ((result (calendar-sync--apply-recurrence-exceptions occurrences nil)))
+ ;; Should return occurrences unchanged or handle nil
+ (should (or (null result)
+ (and (= 1 (length result))
+ (= 9 (nth 3 (plist-get (car result) :start)))))))))
+
+(provide 'test-calendar-sync--apply-recurrence-exceptions)
+;;; test-calendar-sync--apply-recurrence-exceptions.el ends here