summaryrefslogtreecommitdiff
path: root/tests/test-integration-recurring-events.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-18 12:54:25 -0600
committerCraig Jennings <c@cjennings.net>2025-11-18 12:54:25 -0600
commit2da0e8001ae390920f31f1db5e56aa2ed68bf1be (patch)
treeb9075be4442a0528f3eb64869fd4e585c7804219 /tests/test-integration-recurring-events.el
parent342e7df14b688ca995c831a68531550ad59e2cc2 (diff)
feat(calendar-sync): Add RRULE support and refactor expansion functions
Implements complete recurring event (RRULE) expansion for Google Calendar with rolling window approach and comprehensive test coverage. Features: - RRULE expansion for DAILY, WEEKLY, MONTHLY, YEARLY frequencies - Support for INTERVAL, BYDAY, UNTIL, and COUNT parameters - Rolling window: -3 months to +12 months from current date - Fixed COUNT parameter bug (events no longer appear beyond their limit) - Fixed TZID parameter parsing (supports timezone-specific timestamps) - Replaced debug messages with cj/log-silently Refactoring: - Extracted helper functions to eliminate code duplication: - calendar-sync--date-to-time: Date to time conversion - calendar-sync--before-date-p: Date comparison - calendar-sync--create-occurrence: Event occurrence creation - Refactored all expansion functions to use helper functions - Reduced code duplication across daily/weekly/monthly/yearly expansion Testing: - 68 tests total across 5 test files - Unit tests for RRULE parsing, property extraction, weekly expansion - Integration tests for complete RRULE workflow - Tests for helper functions validating refactored code - All tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'tests/test-integration-recurring-events.el')
-rw-r--r--tests/test-integration-recurring-events.el347
1 files changed, 347 insertions, 0 deletions
diff --git a/tests/test-integration-recurring-events.el b/tests/test-integration-recurring-events.el
new file mode 100644
index 00000000..0d32d9e0
--- /dev/null
+++ b/tests/test-integration-recurring-events.el
@@ -0,0 +1,347 @@
+;;; test-integration-recurring-events.el --- Integration tests for recurring events -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Integration tests for the complete recurring event (RRULE) workflow.
+;; Tests the full pipeline: ICS parsing → RRULE expansion → org formatting.
+;;
+;; Components integrated:
+;; - calendar-sync--split-events (ICS event extraction)
+;; - calendar-sync--get-property (property extraction with TZID)
+;; - calendar-sync--parse-rrule (RRULE parsing)
+;; - calendar-sync--expand-weekly/daily/monthly/yearly (event expansion)
+;; - calendar-sync--parse-event (event parsing)
+;; - calendar-sync--event-to-org (org formatting)
+;; - calendar-sync--parse-ics (complete pipeline orchestration)
+;;
+;; This validates that the entire RRULE system works together correctly,
+;; from raw ICS input to final org-mode output.
+
+;;; Code:
+
+(require 'ert)
+(require 'calendar-sync)
+(require 'testutil-calendar-sync)
+
+;;; Setup and Teardown
+
+(defun test-integration-recurring-events-setup ()
+ "Setup for recurring events integration tests."
+ nil)
+
+(defun test-integration-recurring-events-teardown ()
+ "Teardown for recurring events integration tests."
+ nil)
+
+;;; Test Data
+
+(defconst test-integration-recurring-events--weekly-ics
+ "BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Test//Test//EN
+BEGIN:VEVENT
+DTSTART;TZID=America/Chicago:20251118T103000
+DTEND;TZID=America/Chicago:20251118T110000
+RRULE:FREQ=WEEKLY;BYDAY=SA
+SUMMARY:GTFO
+UID:test-weekly@example.com
+END:VEVENT
+END:VCALENDAR"
+ "Test ICS with weekly recurring event (GTFO use case).")
+
+(defconst test-integration-recurring-events--daily-with-count-ics
+ "BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Test//Test//EN
+BEGIN:VEVENT
+DTSTART:20251120T090000Z
+DTEND:20251120T100000Z
+RRULE:FREQ=DAILY;COUNT=5
+SUMMARY:Daily Standup
+UID:test-daily@example.com
+END:VEVENT
+END:VCALENDAR"
+ "Test ICS with daily recurring event limited by COUNT.")
+
+(defconst test-integration-recurring-events--mixed-ics
+ "BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Test//Test//EN
+BEGIN:VEVENT
+DTSTART:20251125T140000Z
+DTEND:20251125T150000Z
+SUMMARY:One-time Meeting
+UID:test-onetime@example.com
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;TZID=America/Chicago:20251201T093000
+DTEND;TZID=America/Chicago:20251201T103000
+RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
+SUMMARY:Recurring Standup
+UID:test-recurring@example.com
+END:VEVENT
+END:VCALENDAR"
+ "Test ICS with mix of recurring and non-recurring events.")
+
+;;; Normal Cases - Complete Workflow
+
+(ert-deftest test-integration-recurring-events-weekly-complete-workflow ()
+ "Test complete workflow for weekly recurring event.
+
+Components integrated:
+- calendar-sync--split-events (extract VEVENT blocks)
+- calendar-sync--get-property (extract DTSTART, DTEND, RRULE with TZID)
+- calendar-sync--parse-rrule (parse FREQ=WEEKLY;BYDAY=SA)
+- calendar-sync--expand-weekly (generate Saturday occurrences)
+- calendar-sync--event-to-org (format as org entries)
+- calendar-sync--parse-ics (orchestrate complete pipeline)
+
+Validates:
+- TZID parameters handled correctly
+- RRULE expansion generates correct dates
+- Multiple occurrences created from single event
+- Org output is properly formatted with timestamps"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let ((org-output (calendar-sync--parse-ics test-integration-recurring-events--weekly-ics)))
+ ;; Should generate org-formatted output
+ (should (stringp org-output))
+ (should (string-match-p "^# Google Calendar Events" org-output))
+
+ ;; Should contain multiple GTFO entries
+ (let ((gtfo-count (with-temp-buffer
+ (insert org-output)
+ (goto-char (point-min))
+ (how-many "^\\* GTFO"))))
+ (should (> gtfo-count 40)) ; ~52 weeks in a year
+ (should (< gtfo-count 60)))
+
+ ;; Should have properly formatted Saturday timestamps
+ (should (string-match-p "<[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} Sat 10:30-11:00>" org-output)))
+ (test-integration-recurring-events-teardown)))
+
+(ert-deftest test-integration-recurring-events-daily-with-count-workflow ()
+ "Test complete workflow for daily recurring event with COUNT limit.
+
+Components integrated:
+- calendar-sync--parse-rrule (with COUNT parameter)
+- calendar-sync--expand-daily (respects COUNT=5)
+- calendar-sync--parse-ics (complete pipeline)
+
+Validates:
+- COUNT parameter limits expansion correctly
+- Daily recurrence generates consecutive days
+- Exactly 5 occurrences created"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let ((org-output (calendar-sync--parse-ics test-integration-recurring-events--daily-with-count-ics)))
+ (should (stringp org-output))
+
+ ;; Should generate exactly 5 Daily Standup entries
+ (let ((standup-count (with-temp-buffer
+ (insert org-output)
+ (goto-char (point-min))
+ (how-many "^\\* Daily Standup"))))
+ (should (= standup-count 5))))
+ (test-integration-recurring-events-teardown)))
+
+(ert-deftest test-integration-recurring-events-mixed-recurring-and-onetime ()
+ "Test workflow with mixed recurring and non-recurring events.
+
+Components integrated:
+- calendar-sync--split-events (handles multiple VEVENT blocks)
+- calendar-sync--expand-recurring-event (detects RRULE vs non-recurring)
+- calendar-sync--parse-event (handles both types)
+- calendar-sync--parse-ics (processes both event types)
+
+Validates:
+- Non-recurring events included once
+- Recurring events expanded correctly
+- Both types appear in output
+- Events are sorted chronologically"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let ((org-output (calendar-sync--parse-ics test-integration-recurring-events--mixed-ics)))
+ (should (stringp org-output))
+
+ ;; Should have one-time meeting
+ (should (string-match-p "^\\* One-time Meeting" org-output))
+
+ ;; Should have multiple recurring standup entries
+ (let ((standup-count (with-temp-buffer
+ (insert org-output)
+ (goto-char (point-min))
+ (how-many "^\\* Recurring Standup"))))
+ (should (> standup-count 10))) ; ~3 days/week for 4 months
+
+ ;; Events should be sorted by date (one-time comes before recurring)
+ (should (< (string-match "One-time Meeting" org-output)
+ (string-match "Recurring Standup" org-output))))
+ (test-integration-recurring-events-teardown)))
+
+;;; Boundary Cases - Date Range Handling
+
+(ert-deftest test-integration-recurring-events-respects-rolling-window ()
+ "Test that RRULE expansion respects rolling window boundaries.
+
+Components integrated:
+- calendar-sync--get-date-range (calculates -3 months to +12 months)
+- calendar-sync--date-in-range-p (filters occurrences)
+- calendar-sync--expand-weekly (respects range)
+- calendar-sync--parse-ics (applies range to all events)
+
+Validates:
+- Events outside date range are excluded
+- Rolling window is applied consistently
+- Past events (> 3 months) excluded
+- Future events (> 12 months) excluded"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let* ((org-output (calendar-sync--parse-ics test-integration-recurring-events--weekly-ics))
+ (now (current-time))
+ (three-months-ago (time-subtract now (* 90 24 3600)))
+ (twelve-months-future (time-add now (* 365 24 3600))))
+ (should (stringp org-output))
+
+ ;; Parse all dates from output
+ (with-temp-buffer
+ (insert org-output)
+ (goto-char (point-min))
+ (let ((all-dates-in-range t))
+ (while (re-search-forward "<\\([0-9]\\{4\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)" nil t)
+ (let* ((year (string-to-number (match-string 1)))
+ (month (string-to-number (match-string 2)))
+ (day (string-to-number (match-string 3)))
+ (event-time (encode-time 0 0 0 day month year)))
+ ;; All dates should be within window
+ (when (or (time-less-p event-time three-months-ago)
+ (time-less-p twelve-months-future event-time))
+ (setq all-dates-in-range nil))))
+ (should all-dates-in-range))))
+ (test-integration-recurring-events-teardown)))
+
+(ert-deftest test-integration-recurring-events-tzid-conversion ()
+ "Test that TZID timestamps are handled correctly throughout pipeline.
+
+Components integrated:
+- calendar-sync--get-property (extracts DTSTART;TZID=America/Chicago:...)
+- calendar-sync--parse-timestamp (converts to local time)
+- calendar-sync--format-timestamp (formats for org-mode)
+- calendar-sync--event-to-org (includes formatted timestamp)
+
+Validates:
+- TZID parameter doesn't break parsing (regression test)
+- Timestamps are correctly formatted in org output
+- Time values are preserved through pipeline"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let ((org-output (calendar-sync--parse-ics test-integration-recurring-events--weekly-ics)))
+ (should (stringp org-output))
+
+ ;; Should have timestamps with time range
+ (should (string-match-p "Sat 10:30-11:00" org-output))
+
+ ;; Should NOT have TZID in output (converted to org format)
+ (should-not (string-match-p "TZID" org-output)))
+ (test-integration-recurring-events-teardown)))
+
+;;; Edge Cases - Error Handling
+
+(ert-deftest test-integration-recurring-events-empty-ics-returns-nil ()
+ "Test that empty ICS content is handled gracefully.
+
+Components integrated:
+- calendar-sync--parse-ics (top-level error handling)
+
+Validates:
+- Empty input doesn't crash
+- Returns nil for empty content"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let ((org-output (calendar-sync--parse-ics "")))
+ (should (null org-output)))
+ (test-integration-recurring-events-teardown)))
+
+(ert-deftest test-integration-recurring-events-malformed-ics-returns-nil ()
+ "Test that malformed ICS content is handled gracefully.
+
+Components integrated:
+- calendar-sync--parse-ics (error handling)
+
+Validates:
+- Malformed input doesn't crash
+- Error is caught and logged
+- Returns nil for malformed content"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let ((org-output (calendar-sync--parse-ics "INVALID ICS DATA")))
+ ;; Should handle error gracefully
+ (should (null org-output)))
+ (test-integration-recurring-events-teardown)))
+
+(ert-deftest test-integration-recurring-events-missing-required-fields ()
+ "Test handling of events missing required fields.
+
+Components integrated:
+- calendar-sync--parse-event (validates required fields)
+- calendar-sync--parse-ics (filters invalid events)
+
+Validates:
+- Events without SUMMARY are excluded
+- Events without DTSTART are excluded
+- Valid events still processed"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let* ((incomplete-ics "BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:20251201T100000Z
+RRULE:FREQ=DAILY;COUNT=2
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:Valid Event
+DTSTART:20251201T110000Z
+DTEND:20251201T120000Z
+END:VEVENT
+END:VCALENDAR")
+ (org-output (calendar-sync--parse-ics incomplete-ics)))
+ ;; Should still generate output (for valid event)
+ (should (stringp org-output))
+ (should (string-match-p "Valid Event" org-output))
+
+ ;; Invalid event (no SUMMARY) should be excluded
+ (should-not (string-match-p "VEVENT" org-output)))
+ (test-integration-recurring-events-teardown)))
+
+(ert-deftest test-integration-recurring-events-unsupported-freq-skipped ()
+ "Test that events with unsupported FREQ are handled gracefully.
+
+Components integrated:
+- calendar-sync--parse-rrule (parses unsupported FREQ)
+- calendar-sync--expand-recurring-event (detects unsupported FREQ)
+- calendar-sync--parse-ics (continues processing other events)
+
+Validates:
+- Unsupported FREQ doesn't crash pipeline
+- Warning message is logged
+- Other events still processed"
+ (test-integration-recurring-events-setup)
+ (unwind-protect
+ (let* ((unsupported-ics "BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:20251201T100000Z
+DTEND:20251201T110000Z
+RRULE:FREQ=HOURLY;COUNT=5
+SUMMARY:Unsupported Hourly Event
+UID:unsupported@example.com
+END:VEVENT
+END:VCALENDAR")
+ (org-output (calendar-sync--parse-ics unsupported-ics)))
+ ;; Should handle gracefully (may return nil or skip the event)
+ ;; The key is it shouldn't crash
+ (should (or (null org-output)
+ (stringp org-output))))
+ (test-integration-recurring-events-teardown)))
+
+(provide 'test-integration-recurring-events)
+;;; test-integration-recurring-events.el ends here