aboutsummaryrefslogtreecommitdiff
path: root/tests/test-gptel-tools-move-to-trash.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-16 11:30:04 -0500
committerCraig Jennings <c@cjennings.net>2026-05-16 11:30:04 -0500
commit244d4c56768fcc60bd1b23fe45df7a57c7b293ec (patch)
tree3a83c5953f7c963c3f3ab7b28044d6decd6ec2fb /tests/test-gptel-tools-move-to-trash.el
parentb35d10bae7315fe3e497f1188bbe5ce86cef1bbf (diff)
downloaddotemacs-244d4c56768fcc60bd1b23fe45df7a57c7b293ec.tar.gz
dotemacs-244d4c56768fcc60bd1b23fe45df7a57c7b293ec.zip
feat(gptel-tools): harden path validation with file-truename realpath
Resolves PATH through file-truename before applying home-directory and read/write checks across the path-handling tools (git_status, git_log, git_diff, move_to_trash, read_text_file, update_text_file, write_text_file, list_directory_files, read_buffer, web_fetch). Without the resolve step, a symlink under HOME pointing outside HOME would pass the prefix check but the tool would act on the real target -- a symlink-escape. move_to_trash also tightens the trash-bin construction (treats empty file extensions correctly) and switches the "critical directories" list to truename-resolved canonical forms so a symlinked ~/.config can't be trashed via an aliased path. update_text_file fixes an off-by-one in the line-count derivation when the source content is empty. Each source change pairs with tests in tests/test-gptel-tools-*.el and tests/test-update-text-file.el covering the realpath escape paths, the empty-extension trash case, and the empty-content line- count edge. Combined coverage is now 100% across all ten gptel-tools source files: 516 / 516 executable lines, 217 tests.
Diffstat (limited to 'tests/test-gptel-tools-move-to-trash.el')
-rw-r--r--tests/test-gptel-tools-move-to-trash.el83
1 files changed, 83 insertions, 0 deletions
diff --git a/tests/test-gptel-tools-move-to-trash.el b/tests/test-gptel-tools-move-to-trash.el
index a6ab1200..77f88627 100644
--- a/tests/test-gptel-tools-move-to-trash.el
+++ b/tests/test-gptel-tools-move-to-trash.el
@@ -91,6 +91,10 @@
"Error: a path outside HOME or /tmp signals."
(should-error (gptel--move-to-trash-validate-path "/etc/hostname")))
+(ert-deftest test-gptel-tools-trash-validate-path-error-tmp-prefix-trick ()
+ "Error: paths that merely start with /tmp are not treated as /tmp children."
+ (should-error (gptel--move-to-trash-validate-path "/tmpnotreally/file")))
+
(ert-deftest test-gptel-tools-trash-validate-path-error-critical-dir ()
"Error: critical directories (home root, .emacs.d, .config, /tmp) signal."
(should-error (gptel--move-to-trash-validate-path "~"))
@@ -107,6 +111,18 @@
(when (file-exists-p path) (delete-file path))
(should-error (gptel--move-to-trash-validate-path path))))
+(ert-deftest test-gptel-tools-trash-validate-path-error-symlink-outside-allowed ()
+ "Error: allowed-location symlinks resolving outside allowed roots are rejected."
+ (let ((link (expand-file-name
+ (format ".test-gptel-tools-trash-outside-link-%s.tmp"
+ (format-time-string "%s%N"))
+ "~")))
+ (unwind-protect
+ (progn
+ (make-symbolic-link "/etc/hostname" link t)
+ (should-error (gptel--move-to-trash-validate-path link)))
+ (when (file-symlink-p link) (delete-file link)))))
+
;; -------------------------- perform
(ert-deftest test-gptel-tools-trash-perform-moves-file ()
@@ -132,5 +148,72 @@
(should-not (file-exists-p dir))
(should (file-exists-p (expand-file-name "subdir/inside.txt" trash))))))))
+(ert-deftest test-gptel-tools-trash-perform-handles-symlink ()
+ "Perform: moving a symlink moves the link, not its target."
+ (test-gptel-tools-trash--with-tmp-tree
+ (lambda (src trash)
+ (let ((target (expand-file-name "target.txt" src))
+ (link (expand-file-name "link.txt" src)))
+ (with-temp-file target (insert "target"))
+ (make-symbolic-link target link t)
+ (let ((status (gptel--move-to-trash-perform link trash)))
+ (should (string-match-p "Symlink moved to trash" status))
+ (should (file-exists-p target))
+ (should-not (file-symlink-p link))
+ (should (file-symlink-p (expand-file-name "link.txt" trash))))))))
+
+(ert-deftest test-gptel-tools-trash-perform-error-rename-failure ()
+ "Error: rename failures are reported with context."
+ (test-gptel-tools-trash--with-tmp-tree
+ (lambda (src trash)
+ (let ((file (expand-file-name "doomed.txt" src)))
+ (with-temp-file file (insert "trash me"))
+ (cl-letf (((symbol-function 'rename-file)
+ (lambda (&rest _args) (error "rename failed"))))
+ (should-error (gptel--move-to-trash-perform file trash)))
+ (should (file-exists-p file))))))
+
+(ert-deftest test-gptel-tools-trash-perform-error-permission-denied ()
+ "Error: permission-denied rename failures get a specific message."
+ (test-gptel-tools-trash--with-tmp-tree
+ (lambda (src trash)
+ (let ((file (expand-file-name "denied.txt" src)))
+ (with-temp-file file (insert "trash me"))
+ (cl-letf (((symbol-function 'rename-file)
+ (lambda (&rest _args)
+ (signal 'permission-denied '("denied")))))
+ (should-error (gptel--move-to-trash-perform file trash)
+ :type 'error))
+ (should (file-exists-p file))))))
+
+(ert-deftest test-gptel-tools-trash-perform-error-original-still-exists ()
+ "Error: post-move verification catches a source path that remains."
+ (test-gptel-tools-trash--with-tmp-tree
+ (lambda (src trash)
+ (let ((file (expand-file-name "still-there.txt" src)))
+ (with-temp-file file (insert "trash me"))
+ (cl-letf (((symbol-function 'rename-file)
+ (lambda (&rest _args) nil)))
+ (should-error (gptel--move-to-trash-perform file trash)))
+ (should (file-exists-p file))))))
+
+(ert-deftest test-gptel-tools-trash-perform-error-trash-missing-after-move ()
+ "Error: post-move verification catches a missing trash target."
+ (test-gptel-tools-trash--with-tmp-tree
+ (lambda (src trash)
+ (let ((file (expand-file-name "missing-trash.txt" src))
+ (real-file-exists-p (symbol-function 'file-exists-p)))
+ (with-temp-file file (insert "trash me"))
+ (cl-letf (((symbol-function 'rename-file)
+ (lambda (&rest _args) nil))
+ ((symbol-function 'file-exists-p)
+ (lambda (path)
+ (cond
+ ((equal path file) nil)
+ ((string-prefix-p trash path) nil)
+ (t (funcall real-file-exists-p path))))))
+ (should-error (gptel--move-to-trash-perform file trash)))
+ (should (funcall real-file-exists-p file))))))
+
(provide 'test-gptel-tools-move-to-trash)
;;; test-gptel-tools-move-to-trash.el ends here