From cc594fdd28f2b047be25b6f016c7f47d23e741ec Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 4 May 2026 00:00:00 -0500 Subject: Make calendar sync startup safe without config --- .gitignore | 1 + modules/calendar-sync.el | 40 +++++--- tests/test-calendar-sync-no-config-startup.el | 130 ++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 14 deletions(-) create mode 100644 tests/test-calendar-sync-no-config-startup.el diff --git a/.gitignore b/.gitignore index af429c22..28b3f21e 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ auto-save-list/ /.org-generic-id-locations /multisession/ /browser-choice.el +/calendar-sync.local.el /client_secret_491339091045-sjje1r54s22vn2ugh45khndjafp89vto.apps.googleusercontent.com.json # reveal.js local clone (managed by scripts/setup-reveal.sh) diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index 06bee213..b232567a 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -69,10 +69,12 @@ ;;; Code: -(require 'user-constants) ; For gcal-file, pcal-file paths - ;;; Configuration +(defgroup calendar-sync nil + "One-way calendar synchronization to Org files." + :group 'calendar) + (defvar calendar-sync-calendars nil "List of calendars to sync. Each calendar is a plist with the following keys: @@ -89,17 +91,13 @@ Example: :url \"https://calendar.proton.me/api/calendar/v1/url/.../calendar.ics\" :file pcal-file)))") -;; Calendar sync (one-way: Google/Proton → Org) -(setq calendar-sync-calendars - `((:name "google" - :url "https://calendar.google.com/calendar/ical/craigmartinjennings%40gmail.com/private-1dad154d6a2100e755f76e2d0502f6aa/basic.ics" - :file ,gcal-file) - (:name "proton" - :url "https://calendar.proton.me/api/calendar/v1/url/MpLtuwsUNoygyA_60GvJE5cz0hbREbrAPBEJoWDRpFEstnmzmEMDb7sjLzkY8kbkF10A7Be3wGKB1-vqaLf-pw==/calendar.ics?CacheKey=LrB9NG5Vfqp5p2sy90H13g%3D%3D&PassphraseKey=sURqFfACPM21d6AXSeaEXYCruimvSb8t0ce1vuxRAXk%3D" - :file ,pcal-file) - (:name "deepsat" - :url "https://calendar.google.com/calendar/ical/craig.jennings%40deepsat.com/private-f0250a2c6752a5ca71d7b0636587a6d5/basic.ics" - :file ,dcal-file))) +(defcustom calendar-sync-private-config-file + (expand-file-name "calendar-sync.local.el" user-emacs-directory) + "Private calendar-sync config file loaded when readable. +This file is the intended place to set `calendar-sync-calendars' with private +calendar feed URLs." + :type 'file + :group 'calendar-sync) (defvar calendar-sync-interval-minutes 60 "Sync interval in minutes. @@ -1229,6 +1227,16 @@ Creates parent directories if needed." ;;; Debug Logging +(defun calendar-sync--load-private-config () + "Load private calendar-sync configuration when available." + (when (file-readable-p calendar-sync-private-config-file) + (condition-case err + (load calendar-sync-private-config-file nil t) + (error + (message "calendar-sync: Failed to load private config %s: %s" + (abbreviate-file-name calendar-sync-private-config-file) + (error-message-string err)))))) + (defun calendar-sync--debug-p () "Return non-nil if calendar-sync debug logging is enabled. Checks `cj/debug-modules' for symbol `calendar-sync' or t (all)." @@ -1446,6 +1454,8 @@ Syncs all calendars immediately, then every `calendar-sync-interval-minutes'." ;;; Initialization +(calendar-sync--load-private-config) + ;; Load saved state from previous session (calendar-sync--load-state) @@ -1465,7 +1475,9 @@ Syncs all calendars immediately, then every `calendar-sync-interval-minutes'." ;; Start auto-sync if enabled and calendars are configured ;; Syncs immediately then every calendar-sync-interval-minutes (default: 60 minutes) -(when (and calendar-sync-auto-start calendar-sync-calendars) +(when (and calendar-sync-auto-start + calendar-sync-calendars + (not noninteractive)) (calendar-sync-start)) (provide 'calendar-sync) diff --git a/tests/test-calendar-sync-no-config-startup.el b/tests/test-calendar-sync-no-config-startup.el new file mode 100644 index 00000000..94a53009 --- /dev/null +++ b/tests/test-calendar-sync-no-config-startup.el @@ -0,0 +1,130 @@ +;;; test-calendar-sync-no-config-startup.el --- No-config startup tests for calendar-sync -*- lexical-binding: t; -*- + +;;; Commentary: +;; Regression tests for loading calendar-sync without configured calendars. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +(defvar calendar-sync-private-config-file) +(defvar calendar-sync-calendars) +(defvar calendar-sync-auto-start) + +(ert-deftest test-calendar-sync-no-config-startup-does-not-start-sync () + "Loading calendar-sync without calendars should not start timers or fetches." + (let ((calendar-sync-private-config-file + (expand-file-name "missing-calendar-sync.local.el" temporary-file-directory)) + (calendar-sync-calendars nil) + (calendar-sync-auto-start t) + (timer-called nil) + (process-called nil) + messages) + (cl-letf (((symbol-function 'run-at-time) + (lambda (&rest _args) + (setq timer-called t) + 'test-calendar-sync-timer)) + ((symbol-function 'make-process) + (lambda (&rest _args) + (setq process-called t) + nil)) + ((symbol-function 'message) + (lambda (format-string &rest args) + (push (apply #'format format-string args) messages)))) + (load (expand-file-name "modules/calendar-sync.el" user-emacs-directory) + nil t)) + (should (boundp 'calendar-sync-calendars)) + (should-not calendar-sync-calendars) + (should-not timer-called) + (should-not process-called) + (should-not (cl-some (lambda (msg) + (string-match-p "calendar-sync: Syncing" msg)) + messages)))) + +(ert-deftest test-calendar-sync-no-config-status-reports-missing-config () + "Status should report missing calendar config without erroring." + (let ((calendar-sync-private-config-file + (expand-file-name "missing-calendar-sync.local.el" temporary-file-directory)) + (calendar-sync-calendars nil) + (calendar-sync-auto-start t) + messages) + (cl-letf (((symbol-function 'run-at-time) (lambda (&rest _args) nil)) + ((symbol-function 'make-process) (lambda (&rest _args) nil)) + ((symbol-function 'message) + (lambda (format-string &rest args) + (push (apply #'format format-string args) messages)))) + (load (expand-file-name "modules/calendar-sync.el" user-emacs-directory) + nil t) + (calendar-sync-status)) + (should (member "calendar-sync: No calendars configured (set calendar-sync-calendars)" + messages)))) + +(ert-deftest test-calendar-sync-no-config-loads-private-config-when-present () + "Loading calendar-sync should read an ignored private config file when present." + (let ((config-file (make-temp-file "calendar-sync-local-" nil ".el")) + (calendar-sync-calendars nil) + (calendar-sync-auto-start t) + (timer-called nil) + (process-called nil)) + (unwind-protect + (progn + (with-temp-file config-file + (insert "(setq calendar-sync-auto-start nil)\n") + (insert "(setq calendar-sync-calendars\n") + (insert " '((:name \"local\" :url \"https://example.test/calendar.ics\" :file \"/tmp/local.org\")))\n")) + (let ((calendar-sync-private-config-file config-file)) + (cl-letf (((symbol-function 'run-at-time) + (lambda (&rest _args) + (setq timer-called t) + 'test-calendar-sync-timer)) + ((symbol-function 'make-process) + (lambda (&rest _args) + (setq process-called t) + nil)) + ((symbol-function 'message) + (lambda (&rest _args) nil))) + (load (expand-file-name "modules/calendar-sync.el" user-emacs-directory) + nil t)) + (should (equal (calendar-sync--calendar-names) '("local"))) + (should-not calendar-sync-auto-start) + (should-not timer-called) + (should-not process-called))) + (delete-file config-file)))) + +(ert-deftest test-calendar-sync-no-config-private-config-does-not-auto-start-in-batch () + "Private config should not auto-start sync while Emacs is noninteractive." + (let ((config-file (make-temp-file "calendar-sync-local-" nil ".el")) + (calendar-sync-calendars nil) + (calendar-sync-auto-start t) + (timer-called nil) + (process-called nil)) + (unwind-protect + (progn + (with-temp-file config-file + (insert "(setq calendar-sync-calendars\n") + (insert " '((:name \"local\" :url \"https://example.test/calendar.ics\" :file \"/tmp/local.org\")))\n")) + (let ((calendar-sync-private-config-file config-file)) + (cl-letf (((symbol-function 'run-at-time) + (lambda (&rest _args) + (setq timer-called t) + 'test-calendar-sync-timer)) + ((symbol-function 'make-process) + (lambda (&rest _args) + (setq process-called t) + nil)) + ((symbol-function 'message) + (lambda (&rest _args) nil))) + (load (expand-file-name "modules/calendar-sync.el" user-emacs-directory) + nil t)) + (should (equal (calendar-sync--calendar-names) '("local"))) + (should calendar-sync-auto-start) + (should noninteractive) + (should-not timer-called) + (should-not process-called))) + (delete-file config-file)))) + +(provide 'test-calendar-sync-no-config-startup) +;;; test-calendar-sync-no-config-startup.el ends here -- cgit v1.2.3