aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-calendar-sync--parse-exception-event.el22
-rw-r--r--tests/test-calendar-sync--syncing-p.el84
-rw-r--r--tests/test-integration-calendar-sync-recurrence-exceptions.el44
-rw-r--r--tests/test-markdown-config.el21
-rw-r--r--tests/test-prog-shell--make-script-executable.el14
-rw-r--r--tests/test-undead-buffers--save-some-buffers-override-no-conflict.el34
-rw-r--r--tests/test-undead-buffers-kill-all-other-buffers-and-windows.el17
7 files changed, 228 insertions, 8 deletions
diff --git a/tests/test-calendar-sync--parse-exception-event.el b/tests/test-calendar-sync--parse-exception-event.el
index 1935d3ebb..a26a7418c 100644
--- a/tests/test-calendar-sync--parse-exception-event.el
+++ b/tests/test-calendar-sync--parse-exception-event.el
@@ -47,6 +47,28 @@
(event (test-calendar-sync-make-vevent "Regular Event" start end)))
(should-not (calendar-sync--parse-exception-event event))))
+;;; Normal Cases — attendee extraction (singly-declined occurrence)
+
+(ert-deftest test-calendar-sync--parse-exception-event-extracts-attendees ()
+ "Normal: a RECURRENCE-ID override carrying an ATTENDEE block parses :attendees,
+so a singly-declined occurrence can have its user status re-derived downstream
+by `calendar-sync--apply-single-exception'."
+ (let* ((start (test-calendar-sync-time-days-from-now 7 10 0))
+ (end (test-calendar-sync-time-days-from-now 7 11 0))
+ (event (concat "BEGIN:VEVENT\n"
+ "UID:override@google.com\n"
+ "RECURRENCE-ID:20260203T090000Z\n"
+ "SUMMARY:1:1 with Hayk\n"
+ "ATTENDEE;CN=Craig;PARTSTAT=DECLINED:mailto:craig@example.com\n"
+ "DTSTART:" (test-calendar-sync-ics-datetime start) "\n"
+ "DTEND:" (test-calendar-sync-ics-datetime end) "\n"
+ "END:VEVENT"))
+ (plist (calendar-sync--parse-exception-event event))
+ (attendees (plist-get plist :attendees)))
+ (should attendees)
+ (should (equal "craig@example.com" (plist-get (car attendees) :email)))
+ (should (equal "DECLINED" (plist-get (car attendees) :partstat)))))
+
;;; Error Cases
(ert-deftest test-calendar-sync--parse-exception-event-error-unparseable-times ()
diff --git a/tests/test-calendar-sync--syncing-p.el b/tests/test-calendar-sync--syncing-p.el
new file mode 100644
index 000000000..b346bf776
--- /dev/null
+++ b/tests/test-calendar-sync--syncing-p.el
@@ -0,0 +1,84 @@
+;;; test-calendar-sync--syncing-p.el --- Tests for the in-flight sync guard -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for `calendar-sync--syncing-p' (the per-calendar in-flight check
+;; that lets the dispatcher skip an overlapping timer tick) and for the
+;; load-state sanitize that clears a stale `syncing' status in a fresh process.
+
+;;; Code:
+
+(require 'ert)
+(require 'calendar-sync)
+
+(defun test-cs-syncing--reset ()
+ "Clear the module's per-calendar state hash."
+ (clrhash calendar-sync--calendar-states))
+
+;;; calendar-sync--syncing-p
+
+(ert-deftest test-calendar-sync--syncing-p-normal-true-when-syncing ()
+ "Normal: a calendar whose status is `syncing' reads as in-flight."
+ (test-cs-syncing--reset)
+ (calendar-sync--set-calendar-state "google" '(:status syncing))
+ (should (calendar-sync--syncing-p "google")))
+
+(ert-deftest test-calendar-sync--syncing-p-boundary-nil-when-no-state ()
+ "Boundary: a calendar with no recorded state is not in-flight."
+ (test-cs-syncing--reset)
+ (should-not (calendar-sync--syncing-p "never-seen")))
+
+(ert-deftest test-calendar-sync--syncing-p-error-nil-for-terminal-status ()
+ "Error: a terminal status (ok / error) is not in-flight."
+ (test-cs-syncing--reset)
+ (calendar-sync--set-calendar-state "google" '(:status ok))
+ (should-not (calendar-sync--syncing-p "google"))
+ (calendar-sync--set-calendar-state "proton" '(:status error))
+ (should-not (calendar-sync--syncing-p "proton")))
+
+;;; Dispatcher guard: an in-flight calendar skips both leaf syncers
+
+(ert-deftest test-calendar-sync--sync-calendar-skips-when-in-flight ()
+ "Normal: `calendar-sync--sync-calendar' does not launch a second sync for a
+calendar already marked syncing, so an overlapping timer tick is a no-op."
+ (test-cs-syncing--reset)
+ (let ((api-calls '()) (ics-calls '()))
+ (cl-letf (((symbol-function 'calendar-sync--sync-calendar-api)
+ (lambda (cal) (push cal api-calls)))
+ ((symbol-function 'calendar-sync--sync-calendar-ics)
+ (lambda (cal) (push cal ics-calls))))
+ (calendar-sync--set-calendar-state "proton" '(:status syncing))
+ (calendar-sync--sync-calendar '(:name "proton" :url "https://x/y.ics"
+ :file "/tmp/c.org"))
+ (should (null api-calls))
+ (should (null ics-calls)))))
+
+(ert-deftest test-calendar-sync--sync-calendar-dispatches-when-idle ()
+ "Boundary: an idle calendar (no in-flight status) still dispatches normally."
+ (test-cs-syncing--reset)
+ (let ((ics-calls '()))
+ (cl-letf (((symbol-function 'calendar-sync--sync-calendar-ics)
+ (lambda (cal) (push cal ics-calls))))
+ (calendar-sync--sync-calendar '(:name "proton" :url "https://x/y.ics"
+ :file "/tmp/c.org"))
+ (should (= 1 (length ics-calls))))))
+
+;;; load-state sanitize: a persisted `syncing' status is cleared on load
+
+(ert-deftest test-calendar-sync--load-state-clears-stale-syncing ()
+ "Error: a `syncing' status persisted before a crash is reset on load, so the
+in-flight guard cannot skip that calendar forever in the new session."
+ (test-cs-syncing--reset)
+ (let* ((dir (make-temp-file "cs-state-" t))
+ (calendar-sync--state-file (expand-file-name "state.el" dir)))
+ (unwind-protect
+ (progn
+ (with-temp-file calendar-sync--state-file
+ (prin1 '((timezone-offset . nil)
+ (calendar-states . (("google" . (:status syncing)))))
+ (current-buffer)))
+ (calendar-sync--load-state)
+ (should-not (calendar-sync--syncing-p "google")))
+ (delete-directory dir t))))
+
+(provide 'test-calendar-sync--syncing-p)
+;;; test-calendar-sync--syncing-p.el ends here
diff --git a/tests/test-integration-calendar-sync-recurrence-exceptions.el b/tests/test-integration-calendar-sync-recurrence-exceptions.el
index 0a9b5af1f..dde0603a2 100644
--- a/tests/test-integration-calendar-sync-recurrence-exceptions.el
+++ b/tests/test-integration-calendar-sync-recurrence-exceptions.el
@@ -162,5 +162,49 @@ Weekly meeting with one instance rescheduled from 09:00 to 10:00."
(should (string-match-p "10:00" org-output))
(should (string-match-p "15:00" org-output))))
+(ert-deftest test-integration-declined-single-occurrence-is-dropped ()
+ "A recurring event with one occurrence declined via a RECURRENCE-ID override
+is filtered out end-to-end, while the other occurrences survive.
+
+This is the singly-declined-occurrence case: declining one instance of a series
+in Google Calendar emits a RECURRENCE-ID override carrying the user's
+PARTSTAT=DECLINED. The override must carry its attendee block all the way from
+`calendar-sync--parse-exception-event' through
+`calendar-sync--apply-single-exception' (status re-derivation) to
+`calendar-sync--filter-declined' for the drop to happen."
+ (let* ((calendar-sync-skip-declined t)
+ (calendar-sync-user-emails '("craig@example.com"))
+ (base-start (test-calendar-sync-time-days-from-now 0 9 0))
+ (base-end (test-calendar-sync-time-days-from-now 0 10 0))
+ (rec-id (test-calendar-sync-time-days-from-now 7 9 0))
+ (decl-start (test-calendar-sync-time-days-from-now 7 9 0))
+ (decl-end (test-calendar-sync-time-days-from-now 7 10 0))
+ (ics-content
+ (concat "BEGIN:VCALENDAR\n"
+ "VERSION:2.0\n"
+ "BEGIN:VEVENT\n"
+ "UID:1on1@google.com\n"
+ "SUMMARY:1on1 with Hayk\n"
+ "DTSTART:" (test-calendar-sync-ics-datetime-local base-start) "\n"
+ "DTEND:" (test-calendar-sync-ics-datetime-local base-end) "\n"
+ "RRULE:FREQ=WEEKLY;COUNT=4\n"
+ "END:VEVENT\n"
+ ;; Week 2 declined: RECURRENCE-ID override with PARTSTAT=DECLINED
+ "BEGIN:VEVENT\n"
+ "UID:1on1@google.com\n"
+ "RECURRENCE-ID:" (test-calendar-sync-ics-datetime-local rec-id) "\n"
+ "SUMMARY:1on1 with Hayk DECLINEDWEEK\n"
+ "ATTENDEE;CN=Craig;PARTSTAT=DECLINED:mailto:craig@example.com\n"
+ "DTSTART:" (test-calendar-sync-ics-datetime-local decl-start) "\n"
+ "DTEND:" (test-calendar-sync-ics-datetime-local decl-end) "\n"
+ "END:VEVENT\n"
+ "END:VCALENDAR"))
+ (org-output (calendar-sync--parse-ics ics-content)))
+ (should (stringp org-output))
+ ;; The non-declined occurrences survive.
+ (should (string-match-p "1on1 with Hayk" org-output))
+ ;; The declined occurrence (unique marker) is dropped.
+ (should-not (string-match-p "DECLINEDWEEK" org-output))))
+
(provide 'test-integration-calendar-sync-recurrence-exceptions)
;;; test-integration-calendar-sync-recurrence-exceptions.el ends here
diff --git a/tests/test-markdown-config.el b/tests/test-markdown-config.el
index edb20d357..e079a8b44 100644
--- a/tests/test-markdown-config.el
+++ b/tests/test-markdown-config.el
@@ -37,10 +37,29 @@
(let ((html (buffer-string)))
(should (string-match-p "<!DOCTYPE html>" html))
(should (string-match-p "<xmp" html))
- (should (string-match-p "strapdown\\.js" html))
(should (string-match-p "some \\*\\*markdown\\*\\*" html)))))
(kill-buffer src))))
+(ert-deftest test-markdown-html-vendors-strapdown-no-external-cdn ()
+ "Normal: the preview embeds the vendored strapdown inline and references no
+external CDN, so the preview works offline and doesn't load third-party JS over
+plain HTTP."
+ (let ((src (generate-new-buffer " *md-cdn*")))
+ (unwind-protect
+ (progn
+ (with-current-buffer src (insert "# Hello"))
+ (with-temp-buffer
+ (cj/markdown-html src)
+ (let ((html (buffer-string)))
+ ;; No external CDN of any kind.
+ (should-not (string-match-p "ndossougbe" html))
+ (should-not (string-match-p "src=\"https?://" html))
+ ;; Vendored strapdown is embedded inline (a bare <script> with the
+ ;; ~121KB bundle, not a <script src=...>).
+ (should (string-match-p "<script>" html))
+ (should (> (length html) 100000)))))
+ (kill-buffer src))))
+
(ert-deftest test-markdown-html-empty-source-buffer ()
"Boundary: an empty source buffer still yields the HTML shell."
(let ((src (generate-new-buffer " *md-empty*")))
diff --git a/tests/test-prog-shell--make-script-executable.el b/tests/test-prog-shell--make-script-executable.el
index e2bb0e6de..0f220b19b 100644
--- a/tests/test-prog-shell--make-script-executable.el
+++ b/tests/test-prog-shell--make-script-executable.el
@@ -106,6 +106,20 @@
(kill-buffer))
(delete-file temp-file))))
+(ert-deftest test-make-script-executable-non-prog-mode-skipped ()
+ "Boundary: a shebang file visited in a non-prog-mode buffer (a script being
+read, quoted, or reviewed) is NOT silently made executable. The auto-exec hook
+runs on every save globally, so it must only act on actual script buffers."
+ (let ((temp-file (test--create-temp-script "#!/bin/bash\necho hello")))
+ (unwind-protect
+ (with-current-buffer (find-file-noselect temp-file)
+ (text-mode)
+ (should-not (test--file-executable-p temp-file))
+ (cj/make-script-executable)
+ (should-not (test--file-executable-p temp-file))
+ (kill-buffer))
+ (delete-file temp-file))))
+
;;; Edge Cases
(ert-deftest test-make-script-executable-edge-no-buffer-file ()
diff --git a/tests/test-undead-buffers--save-some-buffers-override-no-conflict.el b/tests/test-undead-buffers--save-some-buffers-override-no-conflict.el
new file mode 100644
index 000000000..c0dca6e0a
--- /dev/null
+++ b/tests/test-undead-buffers--save-some-buffers-override-no-conflict.el
@@ -0,0 +1,34 @@
+;;; test-undead-buffers--save-some-buffers-override-no-conflict.el --- Regression: save override vs undead -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Regression guard for a launch crash. custom-buffer-file.el installs a
+;; legible `save-some-buffers' override named `cj/save-some-buffers' (arity
+;; arg + pred). undead-buffers.el used to define its own 1-arg
+;; `cj/save-some-buffers' that called `save-some-buffers' internally. In prod
+;; load order (custom-buffer-file before undead-buffers) the 1-arg version won
+;; the symbol, so the override re-entered it with two args and signalled
+;; wrong-number-of-arguments — crashing the kill-all-other-buffers startup path.
+;;
+;; The requires below reproduce that prod order on purpose.
+
+;;; Code:
+
+(require 'ert)
+;; Prod order: override installed first, undead-buffers loaded second.
+(require 'custom-buffer-file)
+(require 'undead-buffers)
+
+(ert-deftest test-undead-buffers-save-override-accepts-predicate ()
+ "Normal: calling `save-some-buffers' with the undead predicate (the
+kill-all-other-buffers path) goes through the legible override without a
+wrong-number-of-arguments crash. No modified file buffers exist in batch, so
+the override returns a count rather than prompting."
+ (should (numberp (save-some-buffers nil #'cj/undead-buffer-p))))
+
+(ert-deftest test-undead-buffers-save-some-buffers-not-arity-shadowed ()
+ "Boundary: `cj/save-some-buffers' must accept the PRED argument the override
+forwards (max arity >= 2), so undead-buffers can't reintroduce a 1-arg shadow."
+ (should (>= (cdr (func-arity #'cj/save-some-buffers)) 2)))
+
+(provide 'test-undead-buffers--save-some-buffers-override-no-conflict)
+;;; test-undead-buffers--save-some-buffers-override-no-conflict.el ends here
diff --git a/tests/test-undead-buffers-kill-all-other-buffers-and-windows.el b/tests/test-undead-buffers-kill-all-other-buffers-and-windows.el
index dcd08e966..36d82add0 100644
--- a/tests/test-undead-buffers-kill-all-other-buffers-and-windows.el
+++ b/tests/test-undead-buffers-kill-all-other-buffers-and-windows.el
@@ -133,23 +133,26 @@
(test-kill-all-other-buffers-and-windows-teardown)))
(ert-deftest test-kill-all-other-buffers-and-windows-should-prompt-for-modified-buffers ()
- "Should call cj/save-some-buffers to handle modified buffers."
+ "Should call save-some-buffers with the undead predicate to handle modified buffers."
(test-kill-all-other-buffers-and-windows-setup)
(unwind-protect
(let ((main (current-buffer))
(file (cj/create-temp-test-file-with-content "original"))
- save-called)
- ;; Mock cj/save-some-buffers to track if it's called
- (cl-letf (((symbol-function 'cj/save-some-buffers)
- (lambda (&optional arg)
- (setq save-called t))))
+ save-called save-pred)
+ ;; Mock save-some-buffers (the standard API the override hooks) to track
+ ;; the call and the predicate passed.
+ (cl-letf (((symbol-function 'save-some-buffers)
+ (lambda (&optional _arg pred &rest _)
+ (setq save-called t
+ save-pred pred))))
(let ((buf (find-file-noselect file)))
(unwind-protect
(progn
(with-current-buffer buf
(insert "modified"))
(cj/kill-all-other-buffers-and-windows)
- (should save-called))
+ (should save-called)
+ (should (eq save-pred #'cj/undead-buffer-p)))
(when (buffer-live-p buf)
(set-buffer-modified-p nil)
(kill-buffer buf))))))