summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-05 15:13:57 -0600
committerCraig Jennings <c@cjennings.net>2026-02-05 15:13:57 -0600
commitb7cb1c51e5663419344d8b55766635801f3ee4c8 (patch)
treea13d903c1d7d82d8b49fe7edbd5f9b7652592c23 /tests
parent12f36cb887c3e84741bc2f3d6afd9e71c6ffddd7 (diff)
feat(calendar-sync): add event details — attendees, organizer, status, URL
Add ICS text unescaping (RFC 5545), HTML stripping, and new fields (attendees/status, organizer, meeting URL) to calendar-sync.el. event-to-org now outputs org property drawers. 88 new tests across 10 test files, 146/146 pass. Also fix pre-existing test require order and keymap guard issues.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-calendar-sync--clean-text.el58
-rw-r--r--tests/test-calendar-sync--convert-tz-to-local.el2
-rw-r--r--tests/test-calendar-sync--event-to-org.el126
-rw-r--r--tests/test-calendar-sync--expand-weekly.el2
-rw-r--r--tests/test-calendar-sync--extract-meeting-url.el54
-rw-r--r--tests/test-calendar-sync--extract-tzid.el1
-rw-r--r--tests/test-calendar-sync--find-user-status.el89
-rw-r--r--tests/test-calendar-sync--get-all-property-lines.el61
-rw-r--r--tests/test-calendar-sync--get-property.el1
-rw-r--r--tests/test-calendar-sync--helpers.el1
-rw-r--r--tests/test-calendar-sync--normalize-line-endings.el1
-rw-r--r--tests/test-calendar-sync--parse-attendee-line.el97
-rw-r--r--tests/test-calendar-sync--parse-organizer.el50
-rw-r--r--tests/test-calendar-sync--parse-rrule.el1
-rw-r--r--tests/test-calendar-sync--strip-html.el99
-rw-r--r--tests/test-calendar-sync--unescape-ics-text.el79
-rw-r--r--tests/test-calendar-sync-properties.el2
-rw-r--r--tests/test-calendar-sync.el2
-rw-r--r--tests/test-integration-calendar-sync-event-details.el177
-rw-r--r--tests/test-org-agenda-build-list.el7
20 files changed, 903 insertions, 7 deletions
diff --git a/tests/test-calendar-sync--clean-text.el b/tests/test-calendar-sync--clean-text.el
new file mode 100644
index 00000000..86c8532b
--- /dev/null
+++ b/tests/test-calendar-sync--clean-text.el
@@ -0,0 +1,58 @@
+;;; test-calendar-sync--clean-text.el --- Tests for clean-text composition -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--clean-text function.
+;; Composes unescape-ics-text + strip-html, trims whitespace. Returns nil for nil.
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--clean-text-normal-both-html-and-ics ()
+ "Test text with both HTML tags and ICS escapes."
+ (should (string= "Hello, World\nNext line"
+ (calendar-sync--clean-text "Hello\\, World<br>Next line"))))
+
+(ert-deftest test-calendar-sync--clean-text-normal-pure-ics-escapes ()
+ "Test text with only ICS escapes."
+ (should (string= "a, b; c"
+ (calendar-sync--clean-text "a\\, b\\; c"))))
+
+(ert-deftest test-calendar-sync--clean-text-normal-pure-html ()
+ "Test text with only HTML."
+ (should (string= "bold and italic"
+ (calendar-sync--clean-text "<b>bold</b> and <i>italic</i>"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--clean-text-boundary-already-clean ()
+ "Test already-clean text passes through unchanged."
+ (should (string= "no escapes here"
+ (calendar-sync--clean-text "no escapes here"))))
+
+(ert-deftest test-calendar-sync--clean-text-boundary-empty-string ()
+ "Test empty string returns empty string."
+ (should (string= "" (calendar-sync--clean-text ""))))
+
+(ert-deftest test-calendar-sync--clean-text-boundary-whitespace-only ()
+ "Test whitespace-only string returns empty after trim."
+ (should (string= "" (calendar-sync--clean-text " \n \t "))))
+
+(ert-deftest test-calendar-sync--clean-text-boundary-leading-trailing-whitespace ()
+ "Test leading/trailing whitespace is trimmed."
+ (should (string= "content"
+ (calendar-sync--clean-text " content "))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--clean-text-error-nil-input ()
+ "Test nil input returns nil."
+ (should (null (calendar-sync--clean-text nil))))
+
+(provide 'test-calendar-sync--clean-text)
+;;; test-calendar-sync--clean-text.el ends here
diff --git a/tests/test-calendar-sync--convert-tz-to-local.el b/tests/test-calendar-sync--convert-tz-to-local.el
index cf45aa61..07822780 100644
--- a/tests/test-calendar-sync--convert-tz-to-local.el
+++ b/tests/test-calendar-sync--convert-tz-to-local.el
@@ -9,8 +9,8 @@
;;; Code:
(require 'ert)
-(require 'calendar-sync)
(require 'testutil-calendar-sync)
+(require 'calendar-sync)
;;; Normal Cases
diff --git a/tests/test-calendar-sync--event-to-org.el b/tests/test-calendar-sync--event-to-org.el
new file mode 100644
index 00000000..e6609e20
--- /dev/null
+++ b/tests/test-calendar-sync--event-to-org.el
@@ -0,0 +1,126 @@
+;;; test-calendar-sync--event-to-org.el --- Tests for updated event-to-org formatter -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--event-to-org function (updated version).
+;; Tests the new property drawer format with LOCATION, ORGANIZER, STATUS, URL.
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--event-to-org-normal-all-fields ()
+ "Test event with all fields produces property drawer + description."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "Team Standup"
+ :start start
+ :end end
+ :description "Daily sync meeting"
+ :location "Conference Room A"
+ :organizer (list :cn "John Smith" :email "john@example.com")
+ :status "accepted"
+ :url "https://meet.google.com/abc-defg-hij")))
+ (let ((result (calendar-sync--event-to-org event)))
+ (should (string-match-p "\\* Team Standup" result))
+ (should (string-match-p ":PROPERTIES:" result))
+ (should (string-match-p ":LOCATION: Conference Room A" result))
+ (should (string-match-p ":ORGANIZER: John Smith" result))
+ (should (string-match-p ":STATUS: accepted" result))
+ (should (string-match-p ":URL: https://meet.google.com/abc-defg-hij" result))
+ (should (string-match-p ":END:" result))
+ (should (string-match-p "Daily sync meeting" result)))))
+
+(ert-deftest test-calendar-sync--event-to-org-normal-summary-and-time-only ()
+ "Test event with only summary and time (no drawer)."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "Quick Chat"
+ :start start
+ :end end)))
+ (let ((result (calendar-sync--event-to-org event)))
+ (should (string-match-p "\\* Quick Chat" result))
+ (should-not (string-match-p ":PROPERTIES:" result)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--event-to-org-boundary-no-description ()
+ "Test event with location but no description."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "Meeting"
+ :start start
+ :end end
+ :location "Room B")))
+ (let ((result (calendar-sync--event-to-org event)))
+ (should (string-match-p ":LOCATION: Room B" result))
+ ;; After :END: there should be no body text
+ (should-not (string-match-p ":END:\n." result)))))
+
+(ert-deftest test-calendar-sync--event-to-org-boundary-no-location-no-attendees ()
+ "Test event without location or attendees produces no drawer."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "Simple Event"
+ :start start
+ :end end
+ :description "Just a note")))
+ (let ((result (calendar-sync--event-to-org event)))
+ (should (string-match-p "\\* Simple Event" result))
+ ;; Description goes after timestamp, no drawer needed
+ (should (string-match-p "Just a note" result)))))
+
+(ert-deftest test-calendar-sync--event-to-org-boundary-only-status ()
+ "Test event with only status produces drawer."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "Status Only"
+ :start start
+ :end end
+ :status "tentative")))
+ (let ((result (calendar-sync--event-to-org event)))
+ (should (string-match-p ":PROPERTIES:" result))
+ (should (string-match-p ":STATUS: tentative" result))
+ (should (string-match-p ":END:" result)))))
+
+(ert-deftest test-calendar-sync--event-to-org-boundary-description-with-asterisks ()
+ "Test event description containing org-special asterisks."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "Meeting"
+ :start start
+ :end end
+ :description "* agenda item 1\n** sub-item")))
+ (let ((result (calendar-sync--event-to-org event)))
+ ;; Description should be present
+ (should (string-match-p "agenda item" result)))))
+
+(ert-deftest test-calendar-sync--event-to-org-boundary-organizer-email-only ()
+ "Test organizer without CN shows email."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "Meeting"
+ :start start
+ :end end
+ :organizer (list :cn nil :email "org@example.com"))))
+ (let ((result (calendar-sync--event-to-org event)))
+ (should (string-match-p ":ORGANIZER: org@example.com" result)))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--event-to-org-error-missing-summary ()
+ "Test event with nil summary still produces output."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary nil
+ :start start
+ :end end)))
+ ;; Should not error - produce some output
+ (should (stringp (calendar-sync--event-to-org event)))))
+
+(provide 'test-calendar-sync--event-to-org)
+;;; test-calendar-sync--event-to-org.el ends here
diff --git a/tests/test-calendar-sync--expand-weekly.el b/tests/test-calendar-sync--expand-weekly.el
index fe333c98..a6143bce 100644
--- a/tests/test-calendar-sync--expand-weekly.el
+++ b/tests/test-calendar-sync--expand-weekly.el
@@ -8,8 +8,8 @@
;;; Code:
(require 'ert)
-(require 'calendar-sync)
(require 'testutil-calendar-sync)
+(require 'calendar-sync)
;;; Setup and Teardown
diff --git a/tests/test-calendar-sync--extract-meeting-url.el b/tests/test-calendar-sync--extract-meeting-url.el
new file mode 100644
index 00000000..2f677991
--- /dev/null
+++ b/tests/test-calendar-sync--extract-meeting-url.el
@@ -0,0 +1,54 @@
+;;; test-calendar-sync--extract-meeting-url.el --- Tests for meeting URL extraction -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--extract-meeting-url function.
+;; Extracts URL from X-GOOGLE-CONFERENCE (preferred) or URL property.
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--extract-meeting-url-normal-google-conference ()
+ "Test extracting X-GOOGLE-CONFERENCE URL."
+ (let ((event "BEGIN:VEVENT\nX-GOOGLE-CONFERENCE:https://meet.google.com/abc-defg-hij\nSUMMARY:Test\nEND:VEVENT"))
+ (should (string= "https://meet.google.com/abc-defg-hij"
+ (calendar-sync--extract-meeting-url event)))))
+
+(ert-deftest test-calendar-sync--extract-meeting-url-normal-url-property ()
+ "Test extracting URL property."
+ (let ((event "BEGIN:VEVENT\nURL:https://zoom.us/j/123456\nSUMMARY:Test\nEND:VEVENT"))
+ (should (string= "https://zoom.us/j/123456"
+ (calendar-sync--extract-meeting-url event)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--extract-meeting-url-boundary-both-present ()
+ "Test X-GOOGLE-CONFERENCE is preferred when both present."
+ (let ((event "BEGIN:VEVENT\nURL:https://zoom.us/j/123456\nX-GOOGLE-CONFERENCE:https://meet.google.com/abc\nSUMMARY:Test\nEND:VEVENT"))
+ (should (string= "https://meet.google.com/abc"
+ (calendar-sync--extract-meeting-url event)))))
+
+(ert-deftest test-calendar-sync--extract-meeting-url-boundary-neither-present ()
+ "Test returns nil when neither URL property exists."
+ (let ((event "BEGIN:VEVENT\nSUMMARY:Test\nDTSTART:20260210T140000Z\nEND:VEVENT"))
+ (should (null (calendar-sync--extract-meeting-url event)))))
+
+(ert-deftest test-calendar-sync--extract-meeting-url-boundary-url-with-params ()
+ "Test URL property with parameters."
+ (let ((event "BEGIN:VEVENT\nURL;VALUE=URI:https://teams.microsoft.com/l/meetup-join/abc\nSUMMARY:Test\nEND:VEVENT"))
+ (should (string-match-p "teams.microsoft.com"
+ (calendar-sync--extract-meeting-url event)))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--extract-meeting-url-error-nil-event ()
+ "Test nil event returns nil."
+ (should (null (calendar-sync--extract-meeting-url nil))))
+
+(provide 'test-calendar-sync--extract-meeting-url)
+;;; test-calendar-sync--extract-meeting-url.el ends here
diff --git a/tests/test-calendar-sync--extract-tzid.el b/tests/test-calendar-sync--extract-tzid.el
index d16aae40..95f51143 100644
--- a/tests/test-calendar-sync--extract-tzid.el
+++ b/tests/test-calendar-sync--extract-tzid.el
@@ -8,6 +8,7 @@
;;; Code:
(require 'ert)
+(require 'testutil-calendar-sync)
(require 'calendar-sync)
;;; Normal Cases
diff --git a/tests/test-calendar-sync--find-user-status.el b/tests/test-calendar-sync--find-user-status.el
new file mode 100644
index 00000000..523f9a9f
--- /dev/null
+++ b/tests/test-calendar-sync--find-user-status.el
@@ -0,0 +1,89 @@
+;;; test-calendar-sync--find-user-status.el --- Tests for find-user-status -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--find-user-status function.
+;; Given attendees list and user emails, returns user's PARTSTAT as lowercase string.
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Test Data
+
+(defun test-find-user-status--make-attendees ()
+ "Create sample attendees list for testing."
+ (list (list :cn "Alice" :email "alice@example.com" :partstat "ACCEPTED" :role "REQ-PARTICIPANT")
+ (list :cn "Craig" :email "craig@example.com" :partstat "ACCEPTED" :role "REQ-PARTICIPANT")
+ (list :cn "Bob" :email "bob@example.com" :partstat "DECLINED" :role "OPT-PARTICIPANT")))
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--find-user-status-normal-accepted ()
+ "Test finding user who accepted."
+ (let ((attendees (test-find-user-status--make-attendees))
+ (emails '("craig@example.com")))
+ (should (string= "accepted"
+ (calendar-sync--find-user-status attendees emails)))))
+
+(ert-deftest test-calendar-sync--find-user-status-normal-declined ()
+ "Test finding user who declined."
+ (let ((attendees (test-find-user-status--make-attendees))
+ (emails '("bob@example.com")))
+ (should (string= "declined"
+ (calendar-sync--find-user-status attendees emails)))))
+
+(ert-deftest test-calendar-sync--find-user-status-normal-tentative ()
+ "Test finding user with tentative status."
+ (let ((attendees (list (list :cn "Test" :email "test@example.com" :partstat "TENTATIVE")))
+ (emails '("test@example.com")))
+ (should (string= "tentative"
+ (calendar-sync--find-user-status attendees emails)))))
+
+(ert-deftest test-calendar-sync--find-user-status-normal-needs-action ()
+ "Test finding user with needs-action status."
+ (let ((attendees (list (list :cn "Test" :email "test@example.com" :partstat "NEEDS-ACTION")))
+ (emails '("test@example.com")))
+ (should (string= "needs-action"
+ (calendar-sync--find-user-status attendees emails)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--find-user-status-boundary-not-in-list ()
+ "Test user not in attendee list returns nil."
+ (let ((attendees (test-find-user-status--make-attendees))
+ (emails '("stranger@example.com")))
+ (should (null (calendar-sync--find-user-status attendees emails)))))
+
+(ert-deftest test-calendar-sync--find-user-status-boundary-empty-attendees ()
+ "Test empty attendee list returns nil."
+ (should (null (calendar-sync--find-user-status '() '("test@example.com")))))
+
+(ert-deftest test-calendar-sync--find-user-status-boundary-multiple-emails ()
+ "Test matching on second email in user emails list."
+ (let ((attendees (test-find-user-status--make-attendees))
+ (emails '("primary@other.com" "craig@example.com")))
+ (should (string= "accepted"
+ (calendar-sync--find-user-status attendees emails)))))
+
+(ert-deftest test-calendar-sync--find-user-status-boundary-case-insensitive ()
+ "Test case-insensitive email matching."
+ (let ((attendees (list (list :cn "Test" :email "Craig@Example.COM" :partstat "ACCEPTED")))
+ (emails '("craig@example.com")))
+ (should (string= "accepted"
+ (calendar-sync--find-user-status attendees emails)))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--find-user-status-error-nil-attendees ()
+ "Test nil attendees returns nil."
+ (should (null (calendar-sync--find-user-status nil '("test@example.com")))))
+
+(ert-deftest test-calendar-sync--find-user-status-error-nil-emails ()
+ "Test nil emails list returns nil."
+ (should (null (calendar-sync--find-user-status (test-find-user-status--make-attendees) nil))))
+
+(provide 'test-calendar-sync--find-user-status)
+;;; test-calendar-sync--find-user-status.el ends here
diff --git a/tests/test-calendar-sync--get-all-property-lines.el b/tests/test-calendar-sync--get-all-property-lines.el
new file mode 100644
index 00000000..c95041c9
--- /dev/null
+++ b/tests/test-calendar-sync--get-all-property-lines.el
@@ -0,0 +1,61 @@
+;;; test-calendar-sync--get-all-property-lines.el --- Tests for get-all-property-lines -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--get-all-property-lines function.
+;; Like get-property-line but returns ALL matching lines.
+;; Needed because ATTENDEE appears multiple times in an event.
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--get-all-property-lines-normal-multiple ()
+ "Test extracting multiple ATTENDEE lines."
+ (let ((event "BEGIN:VEVENT\nATTENDEE;CN=Alice:mailto:alice@example.com\nATTENDEE;CN=Bob:mailto:bob@example.com\nEND:VEVENT"))
+ (let ((result (calendar-sync--get-all-property-lines event "ATTENDEE")))
+ (should (= 2 (length result)))
+ (should (string-match-p "Alice" (nth 0 result)))
+ (should (string-match-p "Bob" (nth 1 result))))))
+
+(ert-deftest test-calendar-sync--get-all-property-lines-normal-single ()
+ "Test extracting single matching line."
+ (let ((event "BEGIN:VEVENT\nATTENDEE;CN=Alice:mailto:alice@example.com\nSUMMARY:Test\nEND:VEVENT"))
+ (let ((result (calendar-sync--get-all-property-lines event "ATTENDEE")))
+ (should (= 1 (length result)))
+ (should (string-match-p "Alice" (car result))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--get-all-property-lines-boundary-no-match ()
+ "Test no matching property returns empty list."
+ (let ((event "BEGIN:VEVENT\nSUMMARY:Test\nEND:VEVENT"))
+ (should (null (calendar-sync--get-all-property-lines event "ATTENDEE")))))
+
+(ert-deftest test-calendar-sync--get-all-property-lines-boundary-continuation ()
+ "Test handling of continuation lines (lines starting with space)."
+ (let ((event "BEGIN:VEVENT\nATTENDEE;CN=Very Long Name;PARTSTAT=ACCEPTED:\n mailto:longname@example.com\nSUMMARY:Test\nEND:VEVENT"))
+ (let ((result (calendar-sync--get-all-property-lines event "ATTENDEE")))
+ (should (= 1 (length result)))
+ (should (string-match-p "longname@example.com" (car result))))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--get-all-property-lines-error-nil-event ()
+ "Test nil event returns nil."
+ (should (null (calendar-sync--get-all-property-lines nil "ATTENDEE"))))
+
+(ert-deftest test-calendar-sync--get-all-property-lines-error-nil-property ()
+ "Test nil property returns nil."
+ (should (null (calendar-sync--get-all-property-lines "BEGIN:VEVENT\nEND:VEVENT" nil))))
+
+(ert-deftest test-calendar-sync--get-all-property-lines-error-empty-event ()
+ "Test empty event string returns nil."
+ (should (null (calendar-sync--get-all-property-lines "" "ATTENDEE"))))
+
+(provide 'test-calendar-sync--get-all-property-lines)
+;;; test-calendar-sync--get-all-property-lines.el ends here
diff --git a/tests/test-calendar-sync--get-property.el b/tests/test-calendar-sync--get-property.el
index 79fefc8f..8b71f8e3 100644
--- a/tests/test-calendar-sync--get-property.el
+++ b/tests/test-calendar-sync--get-property.el
@@ -12,6 +12,7 @@
;;; Code:
(require 'ert)
+(require 'testutil-calendar-sync)
(require 'calendar-sync)
;;; Setup and Teardown
diff --git a/tests/test-calendar-sync--helpers.el b/tests/test-calendar-sync--helpers.el
index eb868952..864faa7f 100644
--- a/tests/test-calendar-sync--helpers.el
+++ b/tests/test-calendar-sync--helpers.el
@@ -7,6 +7,7 @@
;;; Code:
(require 'ert)
+(require 'testutil-calendar-sync)
(require 'calendar-sync)
;;; Setup and Teardown
diff --git a/tests/test-calendar-sync--normalize-line-endings.el b/tests/test-calendar-sync--normalize-line-endings.el
index 7f0830cc..cbada921 100644
--- a/tests/test-calendar-sync--normalize-line-endings.el
+++ b/tests/test-calendar-sync--normalize-line-endings.el
@@ -12,6 +12,7 @@
;;; Code:
(require 'ert)
+(require 'testutil-calendar-sync)
(require 'calendar-sync)
;;; Normal Cases
diff --git a/tests/test-calendar-sync--parse-attendee-line.el b/tests/test-calendar-sync--parse-attendee-line.el
new file mode 100644
index 00000000..5da7aacd
--- /dev/null
+++ b/tests/test-calendar-sync--parse-attendee-line.el
@@ -0,0 +1,97 @@
+;;; test-calendar-sync--parse-attendee-line.el --- Tests for attendee parsing -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--parse-attendee-line function.
+;; Parses single ATTENDEE line into plist (:cn NAME :email EMAIL :partstat STATUS :role ROLE).
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--parse-attendee-line-normal-accepted ()
+ "Test parsing attendee with PARTSTAT=ACCEPTED."
+ (let ((line "ATTENDEE;CN=Alice Smith;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:alice@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (string= "Alice Smith" (plist-get result :cn)))
+ (should (string= "alice@example.com" (plist-get result :email)))
+ (should (string= "ACCEPTED" (plist-get result :partstat)))
+ (should (string= "REQ-PARTICIPANT" (plist-get result :role))))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-normal-declined ()
+ "Test parsing attendee with PARTSTAT=DECLINED."
+ (let ((line "ATTENDEE;CN=Bob Jones;PARTSTAT=DECLINED:mailto:bob@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (string= "Bob Jones" (plist-get result :cn)))
+ (should (string= "bob@example.com" (plist-get result :email)))
+ (should (string= "DECLINED" (plist-get result :partstat))))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-normal-tentative ()
+ "Test parsing attendee with PARTSTAT=TENTATIVE."
+ (let ((line "ATTENDEE;CN=Carol;PARTSTAT=TENTATIVE:mailto:carol@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (string= "TENTATIVE" (plist-get result :partstat))))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-normal-needs-action ()
+ "Test parsing attendee with PARTSTAT=NEEDS-ACTION."
+ (let ((line "ATTENDEE;CN=Dave;PARTSTAT=NEEDS-ACTION:mailto:dave@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (string= "NEEDS-ACTION" (plist-get result :partstat))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--parse-attendee-line-boundary-no-cn ()
+ "Test attendee without CN uses email as fallback."
+ (let ((line "ATTENDEE;PARTSTAT=ACCEPTED:mailto:noCN@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (null (plist-get result :cn)))
+ (should (string= "noCN@example.com" (plist-get result :email))))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-boundary-no-partstat ()
+ "Test attendee without PARTSTAT."
+ (let ((line "ATTENDEE;CN=Eve:mailto:eve@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (string= "Eve" (plist-get result :cn)))
+ (should (string= "eve@example.com" (plist-get result :email)))
+ (should (null (plist-get result :partstat))))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-boundary-minimal ()
+ "Test minimal attendee line with just mailto."
+ (let ((line "ATTENDEE:mailto:min@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (string= "min@example.com" (plist-get result :email))))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-boundary-cutype-resource ()
+ "Test attendee with CUTYPE=RESOURCE."
+ (let ((line "ATTENDEE;CN=Room A;CUTYPE=RESOURCE;PARTSTAT=ACCEPTED:mailto:room-a@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (string= "Room A" (plist-get result :cn)))
+ (should (string= "ACCEPTED" (plist-get result :partstat))))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-boundary-extra-params ()
+ "Test attendee with extra unknown parameters."
+ (let ((line "ATTENDEE;CN=Frank;RSVP=TRUE;X-NUM-GUESTS=0;PARTSTAT=ACCEPTED:mailto:frank@example.com"))
+ (let ((result (calendar-sync--parse-attendee-line line)))
+ (should (string= "Frank" (plist-get result :cn)))
+ (should (string= "ACCEPTED" (plist-get result :partstat))))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--parse-attendee-line-error-nil ()
+ "Test nil input returns nil."
+ (should (null (calendar-sync--parse-attendee-line nil))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-error-empty ()
+ "Test empty string returns nil."
+ (should (null (calendar-sync--parse-attendee-line ""))))
+
+(ert-deftest test-calendar-sync--parse-attendee-line-error-malformed ()
+ "Test malformed line returns nil."
+ (should (null (calendar-sync--parse-attendee-line "not an attendee line"))))
+
+(provide 'test-calendar-sync--parse-attendee-line)
+;;; test-calendar-sync--parse-attendee-line.el ends here
diff --git a/tests/test-calendar-sync--parse-organizer.el b/tests/test-calendar-sync--parse-organizer.el
new file mode 100644
index 00000000..5f21d902
--- /dev/null
+++ b/tests/test-calendar-sync--parse-organizer.el
@@ -0,0 +1,50 @@
+;;; test-calendar-sync--parse-organizer.el --- Tests for organizer parsing -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--parse-organizer function.
+;; Parses ORGANIZER property line into plist (:cn NAME :email EMAIL).
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--parse-organizer-normal-cn-and-email ()
+ "Test parsing organizer with both CN and email."
+ (let ((event "BEGIN:VEVENT\nORGANIZER;CN=John Smith:mailto:john@example.com\nSUMMARY:Test\nEND:VEVENT"))
+ (let ((result (calendar-sync--parse-organizer event)))
+ (should (string= "John Smith" (plist-get result :cn)))
+ (should (string= "john@example.com" (plist-get result :email))))))
+
+(ert-deftest test-calendar-sync--parse-organizer-normal-no-cn ()
+ "Test parsing organizer without CN."
+ (let ((event "BEGIN:VEVENT\nORGANIZER:mailto:organizer@example.com\nSUMMARY:Test\nEND:VEVENT"))
+ (let ((result (calendar-sync--parse-organizer event)))
+ (should (null (plist-get result :cn)))
+ (should (string= "organizer@example.com" (plist-get result :email))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--parse-organizer-boundary-no-organizer ()
+ "Test event without ORGANIZER returns nil."
+ (let ((event "BEGIN:VEVENT\nSUMMARY:Test\nDTSTART:20260210T140000Z\nEND:VEVENT"))
+ (should (null (calendar-sync--parse-organizer event)))))
+
+(ert-deftest test-calendar-sync--parse-organizer-boundary-complex-cn ()
+ "Test organizer with complex CN (quotes, special chars)."
+ (let ((event "BEGIN:VEVENT\nORGANIZER;CN=\"Dr. Jane O'Brien\":mailto:jane@example.com\nSUMMARY:Test\nEND:VEVENT"))
+ (let ((result (calendar-sync--parse-organizer event)))
+ (should (plist-get result :email)))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--parse-organizer-error-nil-event ()
+ "Test nil event returns nil."
+ (should (null (calendar-sync--parse-organizer nil))))
+
+(provide 'test-calendar-sync--parse-organizer)
+;;; test-calendar-sync--parse-organizer.el ends here
diff --git a/tests/test-calendar-sync--parse-rrule.el b/tests/test-calendar-sync--parse-rrule.el
index 123caa5c..099e4e44 100644
--- a/tests/test-calendar-sync--parse-rrule.el
+++ b/tests/test-calendar-sync--parse-rrule.el
@@ -7,6 +7,7 @@
;;; Code:
(require 'ert)
+(require 'testutil-calendar-sync)
(require 'calendar-sync)
;;; Setup and Teardown
diff --git a/tests/test-calendar-sync--strip-html.el b/tests/test-calendar-sync--strip-html.el
new file mode 100644
index 00000000..fda2bbc5
--- /dev/null
+++ b/tests/test-calendar-sync--strip-html.el
@@ -0,0 +1,99 @@
+;;; test-calendar-sync--strip-html.el --- Tests for HTML stripping -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--strip-html function.
+;; Converts <br>/<br/> to newline, strips all other tags, decodes HTML entities.
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--strip-html-normal-br-to-newline ()
+ "Test <br> converted to newline."
+ (should (string= "line1\nline2"
+ (calendar-sync--strip-html "line1<br>line2"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-br-self-closing ()
+ "Test <br/> converted to newline."
+ (should (string= "line1\nline2"
+ (calendar-sync--strip-html "line1<br/>line2"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-br-space-self-closing ()
+ "Test <br /> converted to newline."
+ (should (string= "line1\nline2"
+ (calendar-sync--strip-html "line1<br />line2"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-strip-p-tags ()
+ "Test <p> and </p> tags are stripped."
+ (should (string= "paragraph text"
+ (calendar-sync--strip-html "<p>paragraph text</p>"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-strip-bold ()
+ "Test <b> and </b> tags are stripped."
+ (should (string= "bold text"
+ (calendar-sync--strip-html "<b>bold text</b>"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-combined-tags ()
+ "Test mixed tags are handled."
+ (should (string= "Hello\nWorld"
+ (calendar-sync--strip-html "<p>Hello</p><br><b>World</b>"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-entity-amp ()
+ "Test &amp; decoded to &."
+ (should (string= "A & B"
+ (calendar-sync--strip-html "A &amp; B"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-entity-lt ()
+ "Test &lt; decoded to <."
+ (should (string= "a < b"
+ (calendar-sync--strip-html "a &lt; b"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-entity-gt ()
+ "Test &gt; decoded to >."
+ (should (string= "a > b"
+ (calendar-sync--strip-html "a &gt; b"))))
+
+(ert-deftest test-calendar-sync--strip-html-normal-entity-quot ()
+ "Test &quot; decoded to double quote."
+ (should (string= "say \"hello\""
+ (calendar-sync--strip-html "say &quot;hello&quot;"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--strip-html-boundary-empty-string ()
+ "Test empty string returns empty string."
+ (should (string= "" (calendar-sync--strip-html ""))))
+
+(ert-deftest test-calendar-sync--strip-html-boundary-no-html ()
+ "Test plain text passes through unchanged."
+ (should (string= "just plain text"
+ (calendar-sync--strip-html "just plain text"))))
+
+(ert-deftest test-calendar-sync--strip-html-boundary-only-tags ()
+ "Test string of only tags returns empty."
+ (should (string= ""
+ (calendar-sync--strip-html "<p><b></b></p>"))))
+
+(ert-deftest test-calendar-sync--strip-html-boundary-multiple-br ()
+ "Test multiple consecutive <br> collapse."
+ (should (string-match-p "^line1\n+line2$"
+ (calendar-sync--strip-html "line1<br><br><br>line2"))))
+
+(ert-deftest test-calendar-sync--strip-html-boundary-nested-tags ()
+ "Test nested tags are stripped correctly."
+ (should (string= "nested text"
+ (calendar-sync--strip-html "<div><p><b>nested text</b></p></div>"))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--strip-html-error-nil-input ()
+ "Test nil input returns nil."
+ (should (null (calendar-sync--strip-html nil))))
+
+(provide 'test-calendar-sync--strip-html)
+;;; test-calendar-sync--strip-html.el ends here
diff --git a/tests/test-calendar-sync--unescape-ics-text.el b/tests/test-calendar-sync--unescape-ics-text.el
new file mode 100644
index 00000000..a83e97d3
--- /dev/null
+++ b/tests/test-calendar-sync--unescape-ics-text.el
@@ -0,0 +1,79 @@
+;;; test-calendar-sync--unescape-ics-text.el --- Tests for ICS text unescaping -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--unescape-ics-text function.
+;; RFC 5545 defines escape sequences: \n→newline, \,→comma, \\→backslash, \;→semicolon.
+;; Covers Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--unescape-ics-text-normal-newline ()
+ "Test \\n escape is converted to actual newline."
+ (should (string= "line1\nline2"
+ (calendar-sync--unescape-ics-text "line1\\nline2"))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-normal-comma ()
+ "Test \\, escape is converted to comma."
+ (should (string= "one, two"
+ (calendar-sync--unescape-ics-text "one\\, two"))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-normal-backslash ()
+ "Test \\\\ escape is converted to single backslash."
+ (should (string= "path\\file"
+ (calendar-sync--unescape-ics-text "path\\\\file"))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-normal-semicolon ()
+ "Test \\; escape is converted to semicolon."
+ (should (string= "a;b"
+ (calendar-sync--unescape-ics-text "a\\;b"))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-normal-mixed ()
+ "Test multiple different escapes in one string."
+ (should (string= "Hello, World\nPath\\to;file"
+ (calendar-sync--unescape-ics-text "Hello\\, World\\nPath\\\\to\\;file"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--unescape-ics-text-boundary-empty-string ()
+ "Test empty string returns empty string."
+ (should (string= "" (calendar-sync--unescape-ics-text ""))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-boundary-no-escapes ()
+ "Test string with no escapes passes through unchanged."
+ (should (string= "plain text"
+ (calendar-sync--unescape-ics-text "plain text"))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-boundary-escape-at-start ()
+ "Test escape sequence at string start."
+ (should (string= "\nfoo"
+ (calendar-sync--unescape-ics-text "\\nfoo"))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-boundary-escape-at-end ()
+ "Test escape sequence at string end."
+ (should (string= "foo\n"
+ (calendar-sync--unescape-ics-text "foo\\n"))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-boundary-consecutive-escapes ()
+ "Test consecutive escape sequences."
+ (should (string= "\n\n"
+ (calendar-sync--unescape-ics-text "\\n\\n"))))
+
+(ert-deftest test-calendar-sync--unescape-ics-text-boundary-only-escapes ()
+ "Test string composed entirely of escapes."
+ (should (string= ",;\n\\"
+ (calendar-sync--unescape-ics-text "\\,\\;\\n\\\\"))))
+
+;;; Error Cases
+
+(ert-deftest test-calendar-sync--unescape-ics-text-error-nil-input ()
+ "Test nil input returns nil."
+ (should (null (calendar-sync--unescape-ics-text nil))))
+
+(provide 'test-calendar-sync--unescape-ics-text)
+;;; test-calendar-sync--unescape-ics-text.el ends here
diff --git a/tests/test-calendar-sync-properties.el b/tests/test-calendar-sync-properties.el
index 6054fc5e..c25bb99f 100644
--- a/tests/test-calendar-sync-properties.el
+++ b/tests/test-calendar-sync-properties.el
@@ -19,8 +19,8 @@
;;; Code:
(require 'ert)
-(require 'calendar-sync)
(require 'testutil-calendar-sync)
+(require 'calendar-sync)
(defconst test-calendar-sync-property-trials 30
"Number of random trials to run for each property test.
diff --git a/tests/test-calendar-sync.el b/tests/test-calendar-sync.el
index 7cda5e73..144d6486 100644
--- a/tests/test-calendar-sync.el
+++ b/tests/test-calendar-sync.el
@@ -8,8 +8,8 @@
;;; Code:
(require 'ert)
-(require 'calendar-sync)
(require 'testutil-calendar-sync)
+(require 'calendar-sync)
;;; Test Data
diff --git a/tests/test-integration-calendar-sync-event-details.el b/tests/test-integration-calendar-sync-event-details.el
new file mode 100644
index 00000000..2ecc73e0
--- /dev/null
+++ b/tests/test-integration-calendar-sync-event-details.el
@@ -0,0 +1,177 @@
+;;; test-integration-calendar-sync-event-details.el --- Integration tests for event details -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Integration tests for the enhanced event details workflow.
+;; Tests the complete flow from raw ICS with HTML description, attendees,
+;; organizer through parse-ics to verify org output.
+;;
+;; Components integrated:
+;; - calendar-sync--unescape-ics-text (ICS text unescaping)
+;; - calendar-sync--strip-html (HTML tag removal)
+;; - calendar-sync--clean-text (combined cleaning)
+;; - calendar-sync--get-all-property-lines (multi-line property extraction)
+;; - calendar-sync--parse-attendee-line (attendee parsing)
+;; - calendar-sync--find-user-status (user status lookup)
+;; - calendar-sync--parse-organizer (organizer extraction)
+;; - calendar-sync--extract-meeting-url (meeting URL extraction)
+;; - calendar-sync--parse-event (full event parsing)
+;; - calendar-sync--event-to-org (org format output with property drawer)
+;; - calendar-sync--parse-ics (full ICS pipeline)
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+;;; Test Helpers
+
+(defun test-integration-details--make-full-ics (start-time)
+ "Create ICS with HTML description, attendees, organizer, and meeting URL.
+START-TIME is (year month day hour minute)."
+ (let ((dtstart (test-calendar-sync-ics-datetime start-time))
+ (dtend (test-calendar-sync-ics-datetime
+ (list (nth 0 start-time) (nth 1 start-time) (nth 2 start-time)
+ (1+ (nth 3 start-time)) (nth 4 start-time)))))
+ (concat "BEGIN:VCALENDAR\n"
+ "VERSION:2.0\n"
+ "PRODID:-//Google Inc//Google Calendar//EN\n"
+ "BEGIN:VEVENT\n"
+ "UID:full-test@example.com\n"
+ "SUMMARY:Team Planning\n"
+ "DTSTART:" dtstart "\n"
+ "DTEND:" dtend "\n"
+ "DESCRIPTION:Discuss Q1 goals<br>Review metrics\\, update roadmap\n"
+ "LOCATION:Conference Room B\n"
+ "ORGANIZER;CN=John Smith:mailto:john@example.com\n"
+ "ATTENDEE;CN=John Smith;PARTSTAT=ACCEPTED:mailto:john@example.com\n"
+ "ATTENDEE;CN=Craig Jennings;PARTSTAT=ACCEPTED:mailto:craigmartinjennings@gmail.com\n"
+ "ATTENDEE;CN=Alice;PARTSTAT=DECLINED:mailto:alice@example.com\n"
+ "X-GOOGLE-CONFERENCE:https://meet.google.com/abc-defg-hij\n"
+ "END:VEVENT\n"
+ "END:VCALENDAR")))
+
+(defun test-integration-details--make-plain-ics (start-time)
+ "Create simple ICS without attendees or special properties.
+START-TIME is (year month day hour minute)."
+ (let ((dtstart (test-calendar-sync-ics-datetime start-time))
+ (dtend (test-calendar-sync-ics-datetime
+ (list (nth 0 start-time) (nth 1 start-time) (nth 2 start-time)
+ (1+ (nth 3 start-time)) (nth 4 start-time)))))
+ (concat "BEGIN:VCALENDAR\n"
+ "VERSION:2.0\n"
+ "PRODID:-//Test//Test//EN\n"
+ "BEGIN:VEVENT\n"
+ "UID:plain-test@example.com\n"
+ "SUMMARY:Simple Reminder\n"
+ "DTSTART:" dtstart "\n"
+ "DTEND:" dtend "\n"
+ "END:VEVENT\n"
+ "END:VCALENDAR")))
+
+(defun test-integration-details--make-recurring-with-attendees (start-time)
+ "Create ICS with recurring event that has attendees.
+START-TIME is (year month day hour minute)."
+ (let ((dtstart (test-calendar-sync-ics-datetime start-time))
+ (dtend (test-calendar-sync-ics-datetime
+ (list (nth 0 start-time) (nth 1 start-time) (nth 2 start-time)
+ (1+ (nth 3 start-time)) (nth 4 start-time)))))
+ (concat "BEGIN:VCALENDAR\n"
+ "VERSION:2.0\n"
+ "PRODID:-//Test//Test//EN\n"
+ "BEGIN:VEVENT\n"
+ "UID:recurring-attendees@example.com\n"
+ "SUMMARY:Weekly Standup\n"
+ "DTSTART:" dtstart "\n"
+ "DTEND:" dtend "\n"
+ "RRULE:FREQ=WEEKLY;COUNT=3\n"
+ "ORGANIZER;CN=Manager:mailto:manager@example.com\n"
+ "ATTENDEE;CN=Craig;PARTSTAT=ACCEPTED:mailto:craigmartinjennings@gmail.com\n"
+ "LOCATION:Virtual\n"
+ "END:VEVENT\n"
+ "END:VCALENDAR")))
+
+(defun test-integration-details--make-ics-escaped-location (start-time)
+ "Create ICS with ICS-escaped location field.
+START-TIME is (year month day hour minute)."
+ (let ((dtstart (test-calendar-sync-ics-datetime start-time))
+ (dtend (test-calendar-sync-ics-datetime
+ (list (nth 0 start-time) (nth 1 start-time) (nth 2 start-time)
+ (1+ (nth 3 start-time)) (nth 4 start-time)))))
+ (concat "BEGIN:VCALENDAR\n"
+ "VERSION:2.0\n"
+ "PRODID:-//Test//Test//EN\n"
+ "BEGIN:VEVENT\n"
+ "UID:escaped-loc@example.com\n"
+ "SUMMARY:Offsite Meeting\n"
+ "DTSTART:" dtstart "\n"
+ "DTEND:" dtend "\n"
+ "LOCATION:123 Main St\\, Suite 400\\, New Orleans\\, LA\n"
+ "END:VEVENT\n"
+ "END:VCALENDAR")))
+
+;;; Integration Tests
+
+(ert-deftest test-integration-details-full-pipeline ()
+ "Test full pipeline: ICS with HTML + attendees + organizer → clean org output.
+Verifies:
+- HTML in description is cleaned
+- ICS escapes in description are unescaped
+- Property drawer contains LOCATION, ORGANIZER, STATUS, URL
+- Description appears as body text after drawer"
+ (let* ((start-time (test-calendar-sync-time-days-from-now 7 14 0))
+ (ics (test-integration-details--make-full-ics start-time))
+ (calendar-sync-user-emails '("craigmartinjennings@gmail.com"))
+ (org-output (calendar-sync--parse-ics ics)))
+ (should org-output)
+ ;; Summary present
+ (should (string-match-p "Team Planning" org-output))
+ ;; Clean description (HTML <br> → newline, ICS \, → comma)
+ (should (string-match-p "Discuss Q1 goals" org-output))
+ (should (string-match-p "Review metrics, update roadmap" org-output))
+ (should-not (string-match-p "<br>" org-output))
+ ;; Property drawer
+ (should (string-match-p ":PROPERTIES:" org-output))
+ (should (string-match-p ":LOCATION: Conference Room B" org-output))
+ (should (string-match-p ":ORGANIZER: John Smith" org-output))
+ (should (string-match-p ":STATUS: accepted" org-output))
+ (should (string-match-p ":URL: https://meet.google.com/abc-defg-hij" org-output))
+ (should (string-match-p ":END:" org-output))))
+
+(ert-deftest test-integration-details-plain-event-no-drawer ()
+ "Test plain event without attendees/location produces no property drawer."
+ (let* ((start-time (test-calendar-sync-time-days-from-now 7 14 0))
+ (ics (test-integration-details--make-plain-ics start-time))
+ (org-output (calendar-sync--parse-ics ics)))
+ (should org-output)
+ (should (string-match-p "Simple Reminder" org-output))
+ (should-not (string-match-p ":PROPERTIES:" org-output))))
+
+(ert-deftest test-integration-details-recurring-with-attendees ()
+ "Test recurring event with attendees flows details to expanded instances."
+ (let* ((start-time (test-calendar-sync-time-days-from-now 7 14 0))
+ (ics (test-integration-details--make-recurring-with-attendees start-time))
+ (calendar-sync-user-emails '("craigmartinjennings@gmail.com"))
+ (org-output (calendar-sync--parse-ics ics)))
+ (should org-output)
+ ;; Should have multiple occurrences of Weekly Standup
+ (let ((count 0)
+ (pos 0))
+ (while (string-match "Weekly Standup" org-output pos)
+ (setq count (1+ count))
+ (setq pos (match-end 0)))
+ (should (>= count 2)))))
+
+(ert-deftest test-integration-details-escaped-location ()
+ "Test ICS escapes in location are cleaned in org output."
+ (let* ((start-time (test-calendar-sync-time-days-from-now 7 14 0))
+ (ics (test-integration-details--make-ics-escaped-location start-time))
+ (org-output (calendar-sync--parse-ics ics)))
+ (should org-output)
+ (should (string-match-p "Offsite Meeting" org-output))
+ ;; Location should have real commas, not escaped
+ (should (string-match-p "123 Main St, Suite 400" org-output))
+ (should-not (string-match-p "\\\\," org-output))))
+
+(provide 'test-integration-calendar-sync-event-details)
+;;; test-integration-calendar-sync-event-details.el ends here
diff --git a/tests/test-org-agenda-build-list.el b/tests/test-org-agenda-build-list.el
index 9b9ba7f3..94e89a3d 100644
--- a/tests/test-org-agenda-build-list.el
+++ b/tests/test-org-agenda-build-list.el
@@ -16,6 +16,7 @@
(defvar schedule-file "/tmp/test-schedule.org")
(defvar gcal-file "/tmp/test-gcal.org")
(defvar pcal-file "/tmp/test-pcal.org")
+(defvar dcal-file "/tmp/test-dcal.org")
(defvar projects-dir "/tmp/test-projects/")
;; Now load the actual production module
@@ -184,12 +185,12 @@ When directory scan returns empty:
(cj/build-org-agenda-list)
- ;; Should have base files only (inbox, schedule, gcal, pcal)
- (should (= (length org-agenda-files) 4))
+ ;; Should have base files only (inbox, schedule, gcal, pcal, dcal)
+ (should (= (length org-agenda-files) 5))
;; Cache should contain base files
(should cj/org-agenda-files-cache)
- (should (= (length cj/org-agenda-files-cache) 4)))
+ (should (= (length cj/org-agenda-files-cache) 5)))
(test-org-agenda-teardown)))
(ert-deftest test-org-agenda-build-list-boundary-building-flag-set-during-build ()