summaryrefslogtreecommitdiff
path: root/tests/test-calendar-sync--filter-exdates.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-03 08:09:30 -0600
committerCraig Jennings <c@cjennings.net>2026-02-03 08:09:30 -0600
commit09cfcfd6826f9bc8b379dde88e1d9ca719c1bdb2 (patch)
tree6f062ccc52cf5c334380340ca04d280da3b1df28 /tests/test-calendar-sync--filter-exdates.el
parent03a9247ba4be4803b995510e0e6980254034d7ba (diff)
feat(calendar-sync): add EXDATE support for excluded recurring event dates
When someone deletes a single instance of a recurring meeting in Google Calendar, the calendar exports an EXDATE property marking that date as excluded. Previously, calendar-sync expanded the RRULE without filtering out these excluded dates, causing deleted instances to appear in org output. New functions: - calendar-sync--get-exdates: Extract all EXDATE values from event - calendar-sync--get-exdate-line: Get full EXDATE line with parameters - calendar-sync--parse-exdate: Parse EXDATE into datetime list - calendar-sync--collect-exdates: Collect excluded dates with TZ conversion - calendar-sync--exdate-matches-p: Check if occurrence matches an EXDATE - calendar-sync--filter-exdates: Filter out excluded dates from occurrences Modified calendar-sync--expand-recurring-event to collect and filter EXDATEs after RRULE expansion. Includes 47 new tests covering extraction, parsing, collection, filtering, and integration with RECURRENCE-ID exceptions.
Diffstat (limited to 'tests/test-calendar-sync--filter-exdates.el')
-rw-r--r--tests/test-calendar-sync--filter-exdates.el121
1 files changed, 121 insertions, 0 deletions
diff --git a/tests/test-calendar-sync--filter-exdates.el b/tests/test-calendar-sync--filter-exdates.el
new file mode 100644
index 00000000..b0f2d6a4
--- /dev/null
+++ b/tests/test-calendar-sync--filter-exdates.el
@@ -0,0 +1,121 @@
+;;; test-calendar-sync--filter-exdates.el --- Tests for EXDATE filtering -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--filter-exdates function.
+;; Tests filtering occurrences list to remove EXDATE matches.
+;; 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)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--filter-exdates-normal-single-match-removes-one ()
+ "Test that single matching EXDATE removes one occurrence."
+ (let ((occurrences (list (list :start '(2026 2 3 13 0) :summary "Meeting")
+ (list :start '(2026 2 10 13 0) :summary "Meeting")
+ (list :start '(2026 2 17 13 0) :summary "Meeting")))
+ (exdates '((2026 2 10 13 0))))
+ (let ((result (calendar-sync--filter-exdates occurrences exdates)))
+ (should (= 2 (length result)))
+ ;; Feb 10 should be removed
+ (should-not (cl-find-if (lambda (occ)
+ (equal '(2026 2 10 13 0) (plist-get occ :start)))
+ result))
+ ;; Feb 3 and Feb 17 should remain
+ (should (cl-find-if (lambda (occ)
+ (equal '(2026 2 3 13 0) (plist-get occ :start)))
+ result))
+ (should (cl-find-if (lambda (occ)
+ (equal '(2026 2 17 13 0) (plist-get occ :start)))
+ result)))))
+
+(ert-deftest test-calendar-sync--filter-exdates-normal-multiple-matches-removes-all ()
+ "Test that multiple EXDATEs remove all matching occurrences."
+ (let ((occurrences (list (list :start '(2026 2 3 13 0) :summary "Meeting")
+ (list :start '(2026 2 10 13 0) :summary "Meeting")
+ (list :start '(2026 2 17 13 0) :summary "Meeting")
+ (list :start '(2026 2 24 13 0) :summary "Meeting")))
+ (exdates '((2026 2 10 13 0) (2026 2 24 13 0))))
+ (let ((result (calendar-sync--filter-exdates occurrences exdates)))
+ (should (= 2 (length result)))
+ ;; Feb 10 and Feb 24 should be removed
+ (should-not (cl-find-if (lambda (occ)
+ (equal '(2026 2 10 13 0) (plist-get occ :start)))
+ result))
+ (should-not (cl-find-if (lambda (occ)
+ (equal '(2026 2 24 13 0) (plist-get occ :start)))
+ result)))))
+
+(ert-deftest test-calendar-sync--filter-exdates-normal-preserves-non-matches ()
+ "Test that non-matching occurrences are preserved."
+ (let ((occurrences (list (list :start '(2026 2 3 13 0) :summary "Meeting")
+ (list :start '(2026 2 10 13 0) :summary "Meeting")))
+ (exdates '((2026 3 15 13 0)))) ; No match
+ (let ((result (calendar-sync--filter-exdates occurrences exdates)))
+ (should (= 2 (length result)))
+ ;; Both should remain
+ (should (cl-find-if (lambda (occ)
+ (equal '(2026 2 3 13 0) (plist-get occ :start)))
+ result))
+ (should (cl-find-if (lambda (occ)
+ (equal '(2026 2 10 13 0) (plist-get occ :start)))
+ result)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--filter-exdates-boundary-empty-exdates-returns-all ()
+ "Test that empty exdates list returns all occurrences."
+ (let ((occurrences (list (list :start '(2026 2 3 13 0) :summary "Meeting")
+ (list :start '(2026 2 10 13 0) :summary "Meeting")))
+ (exdates '()))
+ (let ((result (calendar-sync--filter-exdates occurrences exdates)))
+ (should (= 2 (length result))))))
+
+(ert-deftest test-calendar-sync--filter-exdates-boundary-empty-occurrences-returns-empty ()
+ "Test that empty occurrences list returns empty."
+ (let ((occurrences '())
+ (exdates '((2026 2 10 13 0))))
+ (let ((result (calendar-sync--filter-exdates occurrences exdates)))
+ (should (= 0 (length result))))))
+
+(ert-deftest test-calendar-sync--filter-exdates-boundary-all-excluded-returns-empty ()
+ "Test that when all occurrences are excluded, returns empty."
+ (let ((occurrences (list (list :start '(2026 2 3 13 0) :summary "Meeting")
+ (list :start '(2026 2 10 13 0) :summary "Meeting")))
+ (exdates '((2026 2 3 13 0) (2026 2 10 13 0))))
+ (let ((result (calendar-sync--filter-exdates occurrences exdates)))
+ (should (= 0 (length result))))))
+
+(ert-deftest test-calendar-sync--filter-exdates-boundary-date-only-matches-any-time ()
+ "Test that date-only EXDATE (nil hour/minute) matches any time on that day."
+ (let ((occurrences (list (list :start '(2026 2 3 9 0) :summary "Morning")
+ (list :start '(2026 2 3 13 0) :summary "Afternoon")
+ (list :start '(2026 2 10 13 0) :summary "Next Week")))
+ (exdates '((2026 2 3 nil nil)))) ; Date-only exclusion
+ (let ((result (calendar-sync--filter-exdates occurrences exdates)))
+ ;; Both Feb 3 occurrences should be removed
+ (should (= 1 (length result)))
+ (should (equal '(2026 2 10 13 0) (plist-get (car result) :start))))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--filter-exdates-error-nil-occurrences-handles-gracefully ()
+ "Test that nil occurrences handles gracefully."
+ (let ((result (calendar-sync--filter-exdates nil '((2026 2 10 13 0)))))
+ (should (listp result))
+ (should (= 0 (length result)))))
+
+(ert-deftest test-calendar-sync--filter-exdates-error-nil-exdates-returns-occurrences ()
+ "Test that nil exdates returns original occurrences."
+ (let ((occurrences (list (list :start '(2026 2 3 13 0) :summary "Meeting"))))
+ (let ((result (calendar-sync--filter-exdates occurrences nil)))
+ (should (= 1 (length result))))))
+
+(provide 'test-calendar-sync--filter-exdates)
+;;; test-calendar-sync--filter-exdates.el ends here