diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-16 11:30:04 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-16 11:30:04 -0500 |
| commit | 244d4c56768fcc60bd1b23fe45df7a57c7b293ec (patch) | |
| tree | 3a83c5953f7c963c3f3ab7b28044d6decd6ec2fb /tests/test-gptel-tools-move-to-trash.el | |
| parent | b35d10bae7315fe3e497f1188bbe5ce86cef1bbf (diff) | |
| download | dotemacs-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.el | 83 |
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 |
