aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-01 18:05:52 -0500
committerCraig Jennings <c@cjennings.net>2026-06-01 18:05:52 -0500
commit96ac9855cfda77f81d7f12e55d737e8bdd54eccc (patch)
treed295ddb285383be799165c36f840cbc89bf81041
parent49e79a262638ca5fb673ac3f5d8e9cde0cb7905a (diff)
downloadpearl-96ac9855cfda77f81d7f12e55d737e8bdd54eccc.tar.gz
pearl-96ac9855cfda77f81d7f12e55d737e8bdd54eccc.zip
feat(views): guard publish and delete against cross-account local views
I extended the account guard from edit and run to the two remote-mutating commands. pearl-publish-local-view now refuses a local view tagged to another account before any customViewCreate or customViewUpdate, and pearl-delete-local-view refuses before the confirm prompt or any customViewDelete. Publishing or deleting under the wrong account would resolve ids against the wrong workspace or target the wrong remote, so the guard fires at the command boundary, before Linear is touched. publish-current-view inherits the guard by delegating to publish-local-view. This completes the round-6 finding that one guard should cover every read and mutate of a local-view entry, not only edit and copy. Phase 5 of the views spec. Tests confirm both commands error on a cross-account entry and make no Linear call. Suite, compile, and lint are green.
-rw-r--r--pearl.el9
-rw-r--r--tests/test-pearl-local-views.el33
2 files changed, 41 insertions, 1 deletions
diff --git a/pearl.el b/pearl.el
index d984876..38252b1 100644
--- a/pearl.el
+++ b/pearl.el
@@ -3962,7 +3962,11 @@ the id so it can be deleted by hand)."
(message "Cancelled; no local view deleted"))
((not (assoc name pearl-local-views))
(user-error "No local view named %s" name))
- ((not (yes-or-no-p (format "Delete local view %s? " name)))
+ ((progn
+ ;; Refuse a cross-account entry before the confirm or any customViewDelete:
+ ;; deleting under the wrong account could target the wrong remote workspace.
+ (pearl--require-local-view-account name (cdr (assoc name pearl-local-views)))
+ (not (yes-or-no-p (format "Delete local view %s? " name))))
(message "Kept local view %s" name))
(t
(let ((view-id (plist-get (cdr (assoc name pearl-local-views))
@@ -4295,6 +4299,9 @@ the user can re-publish with Replace to reconcile."
(let ((entry (assoc name pearl-local-views)))
(unless entry
(user-error "No local view named %s" name))
+ ;; Refuse a cross-account entry before any customViewCreate/Update: publishing
+ ;; under the wrong account would create the view in the wrong workspace.
+ (pearl--require-local-view-account name (cdr entry))
(let* ((spec (cdr entry))
(filter-plist (plist-get spec :filter))
(existing-view-id (plist-get spec :linear-view-id))
diff --git a/tests/test-pearl-local-views.el b/tests/test-pearl-local-views.el
index 71fb173..b474e74 100644
--- a/tests/test-pearl-local-views.el
+++ b/tests/test-pearl-local-views.el
@@ -167,5 +167,38 @@
(should (equal (plist-get spec :filter) '(:open t)))
(should (equal (plist-get spec :linear-view-id) "v-1")))))))
+;;; account guard on publish and delete -- no Linear call on mismatch (Phase 5)
+
+(ert-deftest test-pearl-publish-local-view-refuses-cross-account ()
+ "Publishing a cross-account local view errors before any customViewCreate/Update."
+ (let ((pearl-local-views '(("v" :filter (:open t) :account "acct-b")))
+ (pearl-accounts '(("acct-a" . x) ("acct-b" . y)))
+ (created nil))
+ (cl-letf (((symbol-function 'pearl--current-account-context)
+ (lambda () '(:name "acct-a")))
+ ((symbol-function 'pearl--require-account-context) (lambda (&rest _) nil))
+ ((symbol-function 'pearl--customview-create-async)
+ (lambda (&rest _) (setq created t)))
+ ((symbol-function 'pearl--customview-update-async)
+ (lambda (&rest _) (setq created t))))
+ (should-error (pearl-publish-local-view "v") :type 'user-error)
+ (should-not created))))
+
+(ert-deftest test-pearl-delete-local-view-refuses-cross-account-no-linear-call ()
+ "Deleting a cross-account local view errors before the confirm or customViewDelete."
+ (let ((pearl-local-views '(("v" :filter (:open t)
+ :linear-view-id "vid" :account "acct-b")))
+ (pearl-accounts '(("acct-a" . x)))
+ (deleted nil) (confirmed nil))
+ (cl-letf (((symbol-function 'pearl--current-account-context)
+ (lambda () '(:name "acct-a")))
+ ((symbol-function 'pearl--customview-delete-async)
+ (lambda (&rest _) (setq deleted t)))
+ ((symbol-function 'yes-or-no-p)
+ (lambda (&rest _) (setq confirmed t) t)))
+ (should-error (pearl-delete-local-view "v") :type 'user-error)
+ (should-not deleted)
+ (should-not confirmed))))
+
(provide 'test-pearl-local-views)
;;; test-pearl-local-views.el ends here