summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-calendar-sync--collect-exdates.el147
-rw-r--r--tests/test-calendar-sync--filter-exdates.el121
-rw-r--r--tests/test-calendar-sync--get-exdates.el121
-rw-r--r--tests/test-calendar-sync--parse-exdate.el80
-rw-r--r--tests/test-integration-calendar-sync-exdate.el207
5 files changed, 676 insertions, 0 deletions
diff --git a/tests/test-calendar-sync--collect-exdates.el b/tests/test-calendar-sync--collect-exdates.el
new file mode 100644
index 00000000..b70277e1
--- /dev/null
+++ b/tests/test-calendar-sync--collect-exdates.el
@@ -0,0 +1,147 @@
+;;; test-calendar-sync--collect-exdates.el --- Tests for EXDATE collection -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--collect-exdates function.
+;; Tests collection of all excluded dates from an event, handling timezone conversion.
+;; 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--collect-exdates-normal-single-returns-list ()
+ "Test collecting single EXDATE returns list with one parsed value."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE:20260210T130000
+SUMMARY:Weekly Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--collect-exdates event)))
+ (should (listp result))
+ (should (= 1 (length result)))
+ (should (equal '(2026 2 10 13 0) (car result))))))
+
+(ert-deftest test-calendar-sync--collect-exdates-normal-multiple-returns-all ()
+ "Test collecting multiple EXDATEs returns all parsed values."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE:20260210T130000
+EXDATE:20260217T130000
+EXDATE:20260224T130000
+SUMMARY:Weekly Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--collect-exdates event)))
+ (should (= 3 (length result)))
+ (should (member '(2026 2 10 13 0) result))
+ (should (member '(2026 2 17 13 0) result))
+ (should (member '(2026 2 24 13 0) result)))))
+
+(ert-deftest test-calendar-sync--collect-exdates-normal-tzid-converts-to-local ()
+ "Test that TZID-qualified EXDATEs are converted to local time."
+ ;; Use a timezone different from local to verify conversion
+ ;; We'll use Europe/London and check that conversion happens
+ (let ((event "BEGIN:VEVENT
+DTSTART;TZID=Europe/London:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE;TZID=Europe/London:20260210T130000
+SUMMARY:London Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--collect-exdates event)))
+ (should (= 1 (length result)))
+ ;; Result should be a valid datetime list (conversion may differ based on local TZ)
+ (let ((parsed (car result)))
+ (should (= 5 (length parsed)))
+ (should (numberp (nth 0 parsed))) ; year
+ (should (numberp (nth 1 parsed))) ; month
+ (should (numberp (nth 2 parsed))) ; day
+ (should (numberp (nth 3 parsed))) ; hour
+ (should (numberp (nth 4 parsed))))))) ; minute
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--collect-exdates-boundary-no-exdates-returns-empty ()
+ "Test that event without EXDATE returns empty list."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+SUMMARY:Weekly Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--collect-exdates event)))
+ (should (listp result))
+ (should (= 0 (length result))))))
+
+(ert-deftest test-calendar-sync--collect-exdates-boundary-utc-converts-to-local ()
+ "Test that UTC (Z suffix) EXDATEs are converted to local time."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T180000Z
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE:20260210T180000Z
+SUMMARY:UTC Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--collect-exdates event)))
+ (should (= 1 (length result)))
+ ;; Result should be converted to local time
+ (let ((parsed (car result)))
+ (should (= 5 (length parsed)))
+ ;; Date should be valid
+ (should (numberp (nth 0 parsed)))))))
+
+(ert-deftest test-calendar-sync--collect-exdates-boundary-mixed-formats-handles-all ()
+ "Test handling mix of TZID, UTC, and local time EXDATEs."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE:20260210T130000
+EXDATE:20260217T180000Z
+SUMMARY:Mixed Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--collect-exdates event)))
+ (should (= 2 (length result)))
+ ;; Both should be valid parsed datetimes
+ (dolist (parsed result)
+ (should (= 5 (length parsed)))
+ (should (numberp (nth 0 parsed)))))))
+
+(ert-deftest test-calendar-sync--collect-exdates-boundary-date-only-returns-date ()
+ "Test collecting all-day EXDATE returns date with nil for time."
+ (let ((event "BEGIN:VEVENT
+DTSTART;VALUE=DATE:20260203
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE;VALUE=DATE:20260210
+SUMMARY:All Day Event
+END:VEVENT"))
+ (let ((result (calendar-sync--collect-exdates event)))
+ (should (= 1 (length result)))
+ (let ((parsed (car result)))
+ (should (equal '(2026 2 10 nil nil) parsed))))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--collect-exdates-error-empty-string-returns-empty ()
+ "Test that empty string returns empty list."
+ (let ((result (calendar-sync--collect-exdates "")))
+ (should (listp result))
+ (should (= 0 (length result)))))
+
+(ert-deftest test-calendar-sync--collect-exdates-error-nil-returns-empty ()
+ "Test that nil input returns empty list."
+ (let ((result (calendar-sync--collect-exdates nil)))
+ (should (listp result))
+ (should (= 0 (length result)))))
+
+(ert-deftest test-calendar-sync--collect-exdates-error-malformed-returns-empty ()
+ "Test that malformed event returns empty list."
+ (let ((result (calendar-sync--collect-exdates "not a vevent")))
+ (should (listp result))
+ (should (= 0 (length result)))))
+
+(provide 'test-calendar-sync--collect-exdates)
+;;; test-calendar-sync--collect-exdates.el ends here
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
diff --git a/tests/test-calendar-sync--get-exdates.el b/tests/test-calendar-sync--get-exdates.el
new file mode 100644
index 00000000..3283bbae
--- /dev/null
+++ b/tests/test-calendar-sync--get-exdates.el
@@ -0,0 +1,121 @@
+;;; test-calendar-sync--get-exdates.el --- Tests for EXDATE extraction -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--get-exdates function.
+;; Tests extraction of EXDATE properties from VEVENT strings.
+;; 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--get-exdates-normal-single-returns-list ()
+ "Test extracting single EXDATE returns list with one value."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE:20260210T130000
+SUMMARY:Weekly Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--get-exdates event)))
+ (should (listp result))
+ (should (= 1 (length result)))
+ (should (string= "20260210T130000" (car result))))))
+
+(ert-deftest test-calendar-sync--get-exdates-normal-multiple-returns-all ()
+ "Test extracting multiple EXDATEs returns all values."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE:20260210T130000
+EXDATE:20260217T130000
+EXDATE:20260224T130000
+SUMMARY:Weekly Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--get-exdates event)))
+ (should (= 3 (length result)))
+ (should (member "20260210T130000" result))
+ (should (member "20260217T130000" result))
+ (should (member "20260224T130000" result)))))
+
+(ert-deftest test-calendar-sync--get-exdates-normal-with-tzid-returns-value ()
+ "Test extracting EXDATE with TZID parameter extracts value correctly."
+ (let ((event "BEGIN:VEVENT
+DTSTART;TZID=America/New_York:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE;TZID=America/New_York:20260210T130000
+SUMMARY:Weekly Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--get-exdates event)))
+ (should (= 1 (length result)))
+ (should (string= "20260210T130000" (car result))))))
+
+(ert-deftest test-calendar-sync--get-exdates-normal-with-z-suffix-returns-value ()
+ "Test extracting UTC EXDATE with Z suffix."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T180000Z
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE:20260210T180000Z
+SUMMARY:Weekly UTC Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--get-exdates event)))
+ (should (= 1 (length result)))
+ (should (string= "20260210T180000Z" (car result))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--get-exdates-boundary-no-exdate-returns-nil ()
+ "Test that event without EXDATE returns nil."
+ (let ((event "BEGIN:VEVENT
+DTSTART:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+SUMMARY:Weekly Meeting
+END:VEVENT"))
+ (should (null (calendar-sync--get-exdates event)))))
+
+(ert-deftest test-calendar-sync--get-exdates-boundary-date-only-returns-value ()
+ "Test extracting all-day EXDATE (date only, no time)."
+ (let ((event "BEGIN:VEVENT
+DTSTART;VALUE=DATE:20260203
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE;VALUE=DATE:20260210
+SUMMARY:All Day Event
+END:VEVENT"))
+ (let ((result (calendar-sync--get-exdates event)))
+ (should (= 1 (length result)))
+ (should (string= "20260210" (car result))))))
+
+(ert-deftest test-calendar-sync--get-exdates-boundary-multiple-params-returns-value ()
+ "Test extracting EXDATE with VALUE=DATE-TIME and TZID parameters."
+ (let ((event "BEGIN:VEVENT
+DTSTART;TZID=America/Chicago:20260203T130000
+RRULE:FREQ=WEEKLY;BYDAY=TU
+EXDATE;VALUE=DATE-TIME;TZID=America/Chicago:20260210T130000
+SUMMARY:Multi-param Meeting
+END:VEVENT"))
+ (let ((result (calendar-sync--get-exdates event)))
+ (should (= 1 (length result)))
+ (should (string= "20260210T130000" (car result))))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--get-exdates-error-empty-string-returns-nil ()
+ "Test that empty string returns nil."
+ (should (null (calendar-sync--get-exdates ""))))
+
+(ert-deftest test-calendar-sync--get-exdates-error-nil-input-returns-nil ()
+ "Test that nil input returns nil."
+ (should (null (calendar-sync--get-exdates nil))))
+
+(ert-deftest test-calendar-sync--get-exdates-error-malformed-returns-nil ()
+ "Test that malformed event string returns nil."
+ (should (null (calendar-sync--get-exdates "not a vevent"))))
+
+(provide 'test-calendar-sync--get-exdates)
+;;; test-calendar-sync--get-exdates.el ends here
diff --git a/tests/test-calendar-sync--parse-exdate.el b/tests/test-calendar-sync--parse-exdate.el
new file mode 100644
index 00000000..2be3d3a1
--- /dev/null
+++ b/tests/test-calendar-sync--parse-exdate.el
@@ -0,0 +1,80 @@
+;;; test-calendar-sync--parse-exdate.el --- Tests for EXDATE parsing -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--parse-exdate function.
+;; Tests parsing EXDATE values into (year month day hour minute) lists.
+;; 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--parse-exdate-normal-datetime-returns-list ()
+ "Test parsing standard datetime format returns correct list."
+ (let ((result (calendar-sync--parse-exdate "20260203T130000")))
+ (should (equal '(2026 2 3 13 0) result))))
+
+(ert-deftest test-calendar-sync--parse-exdate-normal-with-z-returns-list ()
+ "Test parsing UTC datetime with Z suffix returns correct list."
+ (let ((result (calendar-sync--parse-exdate "20260203T180000Z")))
+ (should (equal '(2026 2 3 18 0) result))))
+
+(ert-deftest test-calendar-sync--parse-exdate-normal-date-only-returns-list ()
+ "Test parsing date-only format returns list with nil for time."
+ (let ((result (calendar-sync--parse-exdate "20260203")))
+ (should (equal '(2026 2 3 nil nil) result))))
+
+(ert-deftest test-calendar-sync--parse-exdate-normal-with-seconds-returns-list ()
+ "Test parsing datetime ignores seconds."
+ (let ((result (calendar-sync--parse-exdate "20260203T130045")))
+ (should (equal '(2026 2 3 13 0) result))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--parse-exdate-boundary-midnight-returns-zero-hour ()
+ "Test parsing midnight time returns hour=0."
+ (let ((result (calendar-sync--parse-exdate "20260203T000000")))
+ (should (equal '(2026 2 3 0 0) result))))
+
+(ert-deftest test-calendar-sync--parse-exdate-boundary-end-of-day-returns-23 ()
+ "Test parsing end-of-day time returns hour=23."
+ (let ((result (calendar-sync--parse-exdate "20260203T235900")))
+ (should (equal '(2026 2 3 23 59) result))))
+
+(ert-deftest test-calendar-sync--parse-exdate-boundary-leap-year-feb29-returns-correct ()
+ "Test parsing Feb 29 on leap year."
+ (let ((result (calendar-sync--parse-exdate "20280229T120000")))
+ (should (equal '(2028 2 29 12 0) result))))
+
+(ert-deftest test-calendar-sync--parse-exdate-boundary-new-years-eve-returns-correct ()
+ "Test parsing Dec 31."
+ (let ((result (calendar-sync--parse-exdate "20261231T235900")))
+ (should (equal '(2026 12 31 23 59) result))))
+
+(ert-deftest test-calendar-sync--parse-exdate-boundary-jan-1-returns-correct ()
+ "Test parsing Jan 1."
+ (let ((result (calendar-sync--parse-exdate "20260101T000000")))
+ (should (equal '(2026 1 1 0 0) result))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--parse-exdate-error-empty-returns-nil ()
+ "Test that empty string returns nil."
+ (should (null (calendar-sync--parse-exdate ""))))
+
+(ert-deftest test-calendar-sync--parse-exdate-error-nil-returns-nil ()
+ "Test that nil input returns nil."
+ (should (null (calendar-sync--parse-exdate nil))))
+
+(ert-deftest test-calendar-sync--parse-exdate-error-invalid-format-returns-nil ()
+ "Test that invalid format returns nil."
+ (should (null (calendar-sync--parse-exdate "not-a-date"))))
+
+(provide 'test-calendar-sync--parse-exdate)
+;;; test-calendar-sync--parse-exdate.el ends here
diff --git a/tests/test-integration-calendar-sync-exdate.el b/tests/test-integration-calendar-sync-exdate.el
new file mode 100644
index 00000000..779e0297
--- /dev/null
+++ b/tests/test-integration-calendar-sync-exdate.el
@@ -0,0 +1,207 @@
+;;; test-integration-calendar-sync-exdate.el --- Integration tests for EXDATE support -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Integration tests for end-to-end EXDATE filtering in calendar-sync.
+;; Verifies that excluded dates don't appear in org output.
+;; Following quality-engineer.org guidelines.
+
+;;; 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)
+
+;;; Helper Functions
+
+(defun test-integration-exdate--make-weekly-event-with-exdates (summary start exdates)
+ "Create a weekly recurring event with EXDATES.
+START is (year month day hour minute).
+EXDATES is list of (year month day hour minute) lists to exclude."
+ (let ((dtstart (test-calendar-sync-ics-datetime-local start))
+ (exdate-lines (mapconcat
+ (lambda (ex)
+ (format "EXDATE:%s" (test-calendar-sync-ics-datetime-local ex)))
+ exdates
+ "\n")))
+ (concat "BEGIN:VEVENT\n"
+ "UID:weekly-test@example.com\n"
+ "SUMMARY:" summary "\n"
+ "DTSTART:" dtstart "\n"
+ "DTEND:" (test-calendar-sync-ics-datetime-local
+ (list (nth 0 start) (nth 1 start) (nth 2 start)
+ (1+ (nth 3 start)) (nth 4 start))) "\n"
+ "RRULE:FREQ=WEEKLY;COUNT=4\n"
+ (when (> (length exdates) 0)
+ (concat exdate-lines "\n"))
+ "END:VEVENT")))
+
+(defun test-integration-exdate--date-in-org-output-p (org-output date)
+ "Check if DATE appears in ORG-OUTPUT.
+DATE is (year month day hour minute)."
+ (let ((date-str (format "%04d-%02d-%02d" (nth 0 date) (nth 1 date) (nth 2 date))))
+ (string-match-p (regexp-quote date-str) org-output)))
+
+;;; Normal Cases
+
+(ert-deftest test-integration-exdate-single-excluded-date-not-in-output ()
+ "Test that single excluded date doesn't appear in org output."
+ (let* ((base-start (test-calendar-sync-time-days-from-now 7 13 0))
+ (week2 (test-calendar-sync-time-days-from-now 14 13 0))
+ (week3 (test-calendar-sync-time-days-from-now 21 13 0))
+ (week4 (test-calendar-sync-time-days-from-now 28 13 0))
+ ;; Exclude week 2
+ (event (test-integration-exdate--make-weekly-event-with-exdates
+ "Weekly Sync"
+ base-start
+ (list week2)))
+ (ics-content (test-calendar-sync-make-ics event))
+ (org-output (calendar-sync--parse-ics ics-content)))
+ (should org-output)
+ ;; Week 2 should NOT be in output
+ (should-not (test-integration-exdate--date-in-org-output-p org-output week2))
+ ;; Weeks 1, 3, 4 should be in output
+ (should (test-integration-exdate--date-in-org-output-p org-output base-start))
+ (should (test-integration-exdate--date-in-org-output-p org-output week3))
+ (should (test-integration-exdate--date-in-org-output-p org-output week4))))
+
+(ert-deftest test-integration-exdate-multiple-excluded-dates-filtered ()
+ "Test that multiple excluded dates are all filtered out."
+ (let* ((base-start (test-calendar-sync-time-days-from-now 7 13 0))
+ (week2 (test-calendar-sync-time-days-from-now 14 13 0))
+ (week3 (test-calendar-sync-time-days-from-now 21 13 0))
+ (week4 (test-calendar-sync-time-days-from-now 28 13 0))
+ ;; Exclude weeks 2 and 4
+ (event (test-integration-exdate--make-weekly-event-with-exdates
+ "Weekly Sync"
+ base-start
+ (list week2 week4)))
+ (ics-content (test-calendar-sync-make-ics event))
+ (org-output (calendar-sync--parse-ics ics-content)))
+ (should org-output)
+ ;; Weeks 2 and 4 should NOT be in output
+ (should-not (test-integration-exdate--date-in-org-output-p org-output week2))
+ (should-not (test-integration-exdate--date-in-org-output-p org-output week4))
+ ;; Weeks 1 and 3 should be in output
+ (should (test-integration-exdate--date-in-org-output-p org-output base-start))
+ (should (test-integration-exdate--date-in-org-output-p org-output week3))))
+
+(ert-deftest test-integration-exdate-non-excluded-dates-preserved ()
+ "Test that non-excluded dates remain in output."
+ (let* ((base-start (test-calendar-sync-time-days-from-now 7 13 0))
+ (week2 (test-calendar-sync-time-days-from-now 14 13 0))
+ (week3 (test-calendar-sync-time-days-from-now 21 13 0))
+ (week4 (test-calendar-sync-time-days-from-now 28 13 0))
+ ;; Exclude only week 3
+ (event (test-integration-exdate--make-weekly-event-with-exdates
+ "Weekly Sync"
+ base-start
+ (list week3)))
+ (ics-content (test-calendar-sync-make-ics event))
+ (org-output (calendar-sync--parse-ics ics-content)))
+ (should org-output)
+ ;; Week 3 should NOT be in output
+ (should-not (test-integration-exdate--date-in-org-output-p org-output week3))
+ ;; Weeks 1, 2, 4 should all be preserved
+ (should (test-integration-exdate--date-in-org-output-p org-output base-start))
+ (should (test-integration-exdate--date-in-org-output-p org-output week2))
+ (should (test-integration-exdate--date-in-org-output-p org-output week4))))
+
+;;; Boundary Cases
+
+(ert-deftest test-integration-exdate-no-exdates-all-occurrences-present ()
+ "Test that event without EXDATE shows all dates."
+ (let* ((base-start (test-calendar-sync-time-days-from-now 7 13 0))
+ (week2 (test-calendar-sync-time-days-from-now 14 13 0))
+ (week3 (test-calendar-sync-time-days-from-now 21 13 0))
+ (week4 (test-calendar-sync-time-days-from-now 28 13 0))
+ ;; No exclusions
+ (event (test-integration-exdate--make-weekly-event-with-exdates
+ "Weekly Sync"
+ base-start
+ '())) ; Empty exdates
+ (ics-content (test-calendar-sync-make-ics event))
+ (org-output (calendar-sync--parse-ics ics-content)))
+ (should org-output)
+ ;; All weeks should be present
+ (should (test-integration-exdate--date-in-org-output-p org-output base-start))
+ (should (test-integration-exdate--date-in-org-output-p org-output week2))
+ (should (test-integration-exdate--date-in-org-output-p org-output week3))
+ (should (test-integration-exdate--date-in-org-output-p org-output week4))))
+
+(ert-deftest test-integration-exdate-with-recurrence-id-both-work ()
+ "Test that EXDATE and RECURRENCE-ID work together correctly."
+ ;; Create event with:
+ ;; - Week 2 excluded via EXDATE (completely removed)
+ ;; - Week 3 rescheduled via RECURRENCE-ID (time changed)
+ (let* ((base-start (test-calendar-sync-time-days-from-now 7 13 0))
+ (week2 (test-calendar-sync-time-days-from-now 14 13 0))
+ (week3-original (test-calendar-sync-time-days-from-now 21 13 0))
+ (week3-new (test-calendar-sync-time-days-from-now 21 15 0)) ; Moved to 3pm
+ (week4 (test-calendar-sync-time-days-from-now 28 13 0))
+ ;; Main event with EXDATE for week 2
+ (main-event (concat "BEGIN:VEVENT\n"
+ "UID:combined-test@example.com\n"
+ "SUMMARY:Combined Test\n"
+ "DTSTART:" (test-calendar-sync-ics-datetime-local base-start) "\n"
+ "DTEND:" (test-calendar-sync-ics-datetime-local
+ (list (nth 0 base-start) (nth 1 base-start) (nth 2 base-start)
+ (1+ (nth 3 base-start)) (nth 4 base-start))) "\n"
+ "RRULE:FREQ=WEEKLY;COUNT=4\n"
+ "EXDATE:" (test-calendar-sync-ics-datetime-local week2) "\n"
+ "END:VEVENT"))
+ ;; Exception event rescheduling week 3
+ (exception-event (concat "BEGIN:VEVENT\n"
+ "UID:combined-test@example.com\n"
+ "RECURRENCE-ID:" (test-calendar-sync-ics-datetime-local week3-original) "\n"
+ "SUMMARY:Combined Test (Rescheduled)\n"
+ "DTSTART:" (test-calendar-sync-ics-datetime-local week3-new) "\n"
+ "DTEND:" (test-calendar-sync-ics-datetime-local
+ (list (nth 0 week3-new) (nth 1 week3-new) (nth 2 week3-new)
+ (1+ (nth 3 week3-new)) (nth 4 week3-new))) "\n"
+ "END:VEVENT"))
+ (ics-content (concat "BEGIN:VCALENDAR\n"
+ "VERSION:2.0\n"
+ main-event "\n"
+ exception-event "\n"
+ "END:VCALENDAR"))
+ (org-output (calendar-sync--parse-ics ics-content)))
+ (should org-output)
+ ;; Week 2 should be completely absent (EXDATE)
+ (should-not (test-integration-exdate--date-in-org-output-p org-output week2))
+ ;; Week 3 should have the new time (15:00)
+ (should (string-match-p "15:00" org-output))
+ ;; Weeks 1 and 4 should be present
+ (should (test-integration-exdate--date-in-org-output-p org-output base-start))
+ (should (test-integration-exdate--date-in-org-output-p org-output week4))))
+
+(ert-deftest test-integration-exdate-tzid-conversion-matches-correctly ()
+ "Test that TZID-qualified EXDATE filters correctly after conversion."
+ ;; Use America/New_York timezone
+ (let* ((base-start (test-calendar-sync-time-days-from-now 7 13 0))
+ (week2 (test-calendar-sync-time-days-from-now 14 13 0))
+ (week3 (test-calendar-sync-time-days-from-now 21 13 0))
+ (dtstart-val (format "%04d%02d%02dT%02d%02d00"
+ (nth 0 base-start) (nth 1 base-start) (nth 2 base-start)
+ (nth 3 base-start) (nth 4 base-start)))
+ (exdate-val (format "%04d%02d%02dT%02d%02d00"
+ (nth 0 week2) (nth 1 week2) (nth 2 week2)
+ (nth 3 week2) (nth 4 week2)))
+ (event (concat "BEGIN:VEVENT\n"
+ "UID:tzid-test@example.com\n"
+ "SUMMARY:TZID Test\n"
+ "DTSTART;TZID=America/New_York:" dtstart-val "\n"
+ "RRULE:FREQ=WEEKLY;COUNT=3\n"
+ "EXDATE;TZID=America/New_York:" exdate-val "\n"
+ "END:VEVENT"))
+ (ics-content (test-calendar-sync-make-ics event))
+ (org-output (calendar-sync--parse-ics ics-content)))
+ (should org-output)
+ ;; The EXDATE should have been converted to local time and filtered
+ ;; We can't check exact dates due to TZ conversion, but output should exist
+ ;; and have fewer occurrences than without EXDATE
+ (should (string-match-p "TZID Test" org-output))))
+
+(provide 'test-integration-calendar-sync-exdate)
+;;; test-integration-calendar-sync-exdate.el ends here