diff options
| author | Craig Jennings <c@cjennings.net> | 2025-10-20 11:36:26 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-10-20 11:36:26 -0500 |
| commit | 6ee38f2c4f624f115b61fd63cdcb9b7bf9a6b5a3 (patch) | |
| tree | 52594270c6381a6eb5c6c6315869d8973ca883f3 /tests | |
| parent | af1cbb9454912bb6096917e621b39d4d1082606d (diff) | |
test: dwim-shell-security: Add ERT tests for security
Introduce ERT tests for security-related functions within
dwim-shell-config. These tests encompass normal, boundary, and error
conditions to ensure:
- Passwords aren't exposed in command outputs
- Temporary files have restricted permissions (mode 600)
- Proper cleanup of temporary files post-operation
- Handling of edge cases and errors effectively
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-dwim-shell-security.el | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/tests/test-dwim-shell-security.el b/tests/test-dwim-shell-security.el new file mode 100644 index 00000000..0151a7c7 --- /dev/null +++ b/tests/test-dwim-shell-security.el @@ -0,0 +1,341 @@ +;;; test-dwim-shell-security.el --- ERT tests for dwim-shell-config security functions -*- lexical-binding: t; -*- + +;; Author: Claude Code and cjennings +;; Keywords: tests, dwim-shell, security + +;;; Commentary: +;; ERT tests for security-related dwim-shell-config.el functions. +;; Tests are organized into normal, boundary, and error cases. +;; +;; These tests verify that password-protected operations: +;; - Do not expose passwords in process lists or command output +;; - Use temporary files with restrictive permissions (mode 600) +;; - Clean up temporary files after use (even on error) +;; - Properly handle edge cases and errors + +;;; Code: + +(require 'ert) +(require 'dwim-shell-config) +(require 'testutil-general) + +;;; Setup and Teardown + +(defun test-dwim-shell-security-setup () + "Set up test environment for dwim-shell-security tests." + (cj/create-test-base-dir) + ;; Create test PDF file + (setq test-pdf-file (expand-file-name "test.pdf" cj/test-base-dir)) + ;; Create minimal valid PDF (this is a minimal PDF structure) + (with-temp-file test-pdf-file + (insert "%PDF-1.4\n") + (insert "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n") + (insert "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n") + (insert "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n") + (insert "xref\n0 4\n") + (insert "0000000000 65535 f\n") + (insert "0000000009 00000 n\n") + (insert "0000000058 00000 n\n") + (insert "0000000115 00000 n\n") + (insert "trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n203\n%%EOF\n")) + ;; Create test files for archive operations + (setq test-file-1 (expand-file-name "file1.txt" cj/test-base-dir)) + (setq test-file-2 (expand-file-name "file2.txt" cj/test-base-dir)) + (with-temp-file test-file-1 (insert "Test content 1")) + (with-temp-file test-file-2 (insert "Test content 2"))) + +(defun test-dwim-shell-security-teardown () + "Clean up test environment after dwim-shell-security tests." + ;; Clean up test directory + (cj/delete-test-base-dir)) + +;;; Helper Functions + +(defun test-dwim-check-temp-file-cleanup (pattern) + "Check that no temporary files matching PATTERN remain after operation." + (let ((temp-files (directory-files temporary-file-directory nil pattern))) + (should (null temp-files)))) + +(defun test-dwim-check-file-permissions (file expected-mode) + "Check that FILE has EXPECTED-MODE permissions." + (when (file-exists-p file) + (should (equal (file-modes file) expected-mode)))) + +;;; Normal Cases - PDF Password Protect + +(ert-deftest test-dwim-pdf-password-protect-creates-temp-file-normal () + "Normal: PDF password protect creates temporary file with secure permissions." + (skip-unless (executable-find "qpdf")) + (test-dwim-shell-security-setup) + (unwind-protect + (let* ((captured-temp-file nil) + (original-make-temp-file (symbol-function 'make-temp-file))) + ;; Wrap make-temp-file to capture the temp file path + (cl-letf (((symbol-function 'make-temp-file) + (lambda (&rest args) + (setq captured-temp-file (apply original-make-temp-file args)) + captured-temp-file)) + ;; Mock read-passwd to avoid interactive prompts + ((symbol-function 'read-passwd) + (lambda (_prompt) "test-password")) + ;; Mock dwim-shell-command-on-marked-files to check behavior + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description command &rest _args) + ;; Verify temp file exists with correct permissions during execution + (should (file-exists-p captured-temp-file)) + (test-dwim-check-file-permissions captured-temp-file #o600) + ;; Verify password is in temp file, not in command + (should (string-match-p captured-temp-file command)) + (should-not (string-match-p "test-password" command))))) + (cj/dwim-shell-commands-pdf-password-protect) + ;; Verify temp file is cleaned up after operation + (should-not (file-exists-p captured-temp-file)))) + (test-dwim-shell-security-teardown))) + +(ert-deftest test-dwim-pdf-password-protect-no-password-in-command-normal () + "Normal: Password does not appear in shell command string." + (skip-unless (executable-find "qpdf")) + (test-dwim-shell-security-setup) + (unwind-protect + (let ((test-password "SuperSecret123!")) + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) test-password)) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description command &rest _args) + ;; Password should NOT appear in command + (should-not (string-match-p test-password command)) + ;; Command should reference password file + (should (string-match-p "--password-file=" command))))) + (cj/dwim-shell-commands-pdf-password-protect))) + (test-dwim-shell-security-teardown))) + +;;; Normal Cases - PDF Password Unprotect + +(ert-deftest test-dwim-pdf-password-unprotect-creates-temp-file-normal () + "Normal: PDF password unprotect creates temporary file with secure permissions." + (skip-unless (executable-find "qpdf")) + (test-dwim-shell-security-setup) + (unwind-protect + (let* ((captured-temp-file nil) + (original-make-temp-file (symbol-function 'make-temp-file))) + (cl-letf (((symbol-function 'make-temp-file) + (lambda (&rest args) + (setq captured-temp-file (apply original-make-temp-file args)) + captured-temp-file)) + ((symbol-function 'read-passwd) + (lambda (_prompt) "test-password")) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description command &rest _args) + (should (file-exists-p captured-temp-file)) + (test-dwim-check-file-permissions captured-temp-file #o600) + (should (string-match-p captured-temp-file command)) + (should-not (string-match-p "test-password" command))))) + (cj/dwim-shell-commands-pdf-password-unprotect) + (should-not (file-exists-p captured-temp-file)))) + (test-dwim-shell-security-teardown))) + +;;; Normal Cases - Create Encrypted Archive + +(ert-deftest test-dwim-create-encrypted-zip-uses-7z-normal () + "Normal: Create encrypted archive uses 7z, not zip." + (skip-unless (executable-find "7z")) + (test-dwim-shell-security-setup) + (unwind-protect + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) "test-password")) + ((symbol-function 'read-string) + (lambda (_prompt &optional _default) "test-archive")) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description command &rest args) + ;; Should use 7z, not zip + (should (string-match-p "7z a" command)) + (should-not (string-match-p "zip -" command)) + ;; Should use AES encryption + (should (string-match-p "-mhe=on" command)) + ;; Verify utils parameter is 7z + (should (equal (plist-get args :utils) "7z"))))) + (cj/dwim-shell-commands-create-encrypted-zip)) + (test-dwim-shell-security-teardown))) + +(ert-deftest test-dwim-create-encrypted-zip-no-password-in-command-normal () + "Normal: Password does not appear in shell command string for archive creation." + (skip-unless (executable-find "7z")) + (test-dwim-shell-security-setup) + (unwind-protect + (let ((test-password "VerySecret456!")) + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) test-password)) + ((symbol-function 'read-string) + (lambda (_prompt &optional _default) "test-archive")) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description command &rest _args) + ;; Password should NOT appear directly in command + (should-not (string-match-p test-password command)) + ;; Should use cat to read from temp file + (should (string-match-p "cat" command))))) + (cj/dwim-shell-commands-create-encrypted-zip))) + (test-dwim-shell-security-teardown))) + +;;; Normal Cases - Remove Archive Encryption + +(ert-deftest test-dwim-remove-zip-encryption-uses-7z-normal () + "Normal: Remove archive encryption uses 7z for both extract and create." + (skip-unless (executable-find "7z")) + (test-dwim-shell-security-setup) + (unwind-protect + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) "test-password")) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description command &rest args) + ;; Should use 7z for both extract and archive + (should (string-match-p "7z x" command)) + (should (string-match-p "7z a" command)) + ;; Verify utils parameter is 7z + (should (equal (plist-get args :utils) "7z"))))) + (cj/dwim-shell-commands-remove-zip-encryption)) + (test-dwim-shell-security-teardown))) + +;;; Boundary Cases + +(ert-deftest test-dwim-pdf-password-empty-password-boundary () + "Boundary: Empty password is accepted (though qpdf may reject it)." + (skip-unless (executable-find "qpdf")) + (test-dwim-shell-security-setup) + (unwind-protect + (let ((command-executed nil)) + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) "")) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description _command &rest _args) + (setq command-executed t)))) + (cj/dwim-shell-commands-pdf-password-protect) + ;; Function should accept empty password (tool may reject later) + (should command-executed))) + (test-dwim-shell-security-teardown))) + +(ert-deftest test-dwim-pdf-password-special-characters-boundary () + "Boundary: Password with special characters is properly handled." + (skip-unless (executable-find "qpdf")) + (test-dwim-shell-security-setup) + (unwind-protect + (let ((special-password "p@$$w0rd!#%^&*()")) + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) special-password)) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description command &rest _args) + ;; Special characters should not appear in command + (should-not (string-match-p (regexp-quote special-password) command))))) + (cj/dwim-shell-commands-pdf-password-protect))) + (test-dwim-shell-security-teardown))) + +(ert-deftest test-dwim-archive-very-long-password-boundary () + "Boundary: Very long password (1000+ chars) is properly handled." + (skip-unless (executable-find "7z")) + (test-dwim-shell-security-setup) + (unwind-protect + (let ((long-password (make-string 1000 ?x)) + (captured-temp-file nil)) + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) long-password)) + ((symbol-function 'read-string) + (lambda (_prompt &optional _default) "test")) + ((symbol-function 'make-temp-file) + (lambda (&rest args) + (setq captured-temp-file (apply (symbol-function 'make-temp-file) args)) + captured-temp-file)) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description _command &rest _args) + ;; Verify password was written to temp file + (with-temp-buffer + (insert-file-contents captured-temp-file) + (should (equal (buffer-string) long-password)))))) + (cj/dwim-shell-commands-create-encrypted-zip))) + (test-dwim-shell-security-teardown))) + +;;; Error Cases + +(ert-deftest test-dwim-pdf-password-temp-file-cleanup-on-error-error () + "Error: Temporary file is cleaned up even when command fails." + (skip-unless (executable-find "qpdf")) + (test-dwim-shell-security-setup) + (unwind-protect + (let* ((captured-temp-file nil) + (original-make-temp-file (symbol-function 'make-temp-file))) + (cl-letf (((symbol-function 'make-temp-file) + (lambda (&rest args) + (setq captured-temp-file (apply original-make-temp-file args)) + captured-temp-file)) + ((symbol-function 'read-passwd) + (lambda (_prompt) "test-password")) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description _command &rest _args) + ;; Simulate command failure + (error "Command failed")))) + ;; Should error, but still clean up temp file + (should-error (cj/dwim-shell-commands-pdf-password-protect)) + ;; Temp file should be cleaned up despite error + (should-not (file-exists-p captured-temp-file)))) + (test-dwim-shell-security-teardown))) + +(ert-deftest test-dwim-archive-temp-file-cleanup-on-error-error () + "Error: Archive temp file cleaned up even when 7z command fails." + (skip-unless (executable-find "7z")) + (test-dwim-shell-security-setup) + (unwind-protect + (let* ((captured-temp-file nil) + (original-make-temp-file (symbol-function 'make-temp-file))) + (cl-letf (((symbol-function 'make-temp-file) + (lambda (&rest args) + (setq captured-temp-file (apply original-make-temp-file args)) + captured-temp-file)) + ((symbol-function 'read-passwd) + (lambda (_prompt) "test-password")) + ((symbol-function 'read-string) + (lambda (_prompt &optional _default) "test")) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (_description _command &rest _args) + (error "7z command failed")))) + (should-error (cj/dwim-shell-commands-create-encrypted-zip)) + (should-not (file-exists-p captured-temp-file)))) + (test-dwim-shell-security-teardown))) + +(ert-deftest test-dwim-pdf-password-temp-file-write-error-error () + "Error: Error when unable to write to temporary file." + (skip-unless (executable-find "qpdf")) + (test-dwim-shell-security-setup) + (unwind-protect + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) "test-password")) + ;; Mock make-temp-file to return a path that can't be written + ((symbol-function 'make-temp-file) + (lambda (&rest _args) "/nonexistent/path/temp-file"))) + ;; Should error when trying to write to non-existent path + (should-error (cj/dwim-shell-commands-pdf-password-protect))) + (test-dwim-shell-security-teardown))) + +(ert-deftest test-dwim-multiple-temp-file-cleanup-error () + "Error: Multiple operations don't leave temp files behind." + (skip-unless (and (executable-find "qpdf") (executable-find "7z"))) + (test-dwim-shell-security-setup) + (unwind-protect + (progn + ;; Track temp files before operations + (let ((initial-temp-files (directory-files temporary-file-directory nil "^qpdf-pass-\\|^7z-pass-"))) + (cl-letf (((symbol-function 'read-passwd) + (lambda (_prompt) "password")) + ((symbol-function 'read-string) + (lambda (_prompt &optional _default) "archive")) + ((symbol-function 'dwim-shell-command-on-marked-files) + (lambda (&rest _args) nil))) + ;; Run multiple operations + (cj/dwim-shell-commands-pdf-password-protect) + (cj/dwim-shell-commands-pdf-password-unprotect) + (cj/dwim-shell-commands-create-encrypted-zip) + (cj/dwim-shell-commands-remove-zip-encryption)) + ;; Check no new temp files remain + (let ((final-temp-files (directory-files temporary-file-directory nil "^qpdf-pass-\\|^7z-pass-"))) + (should (equal (length final-temp-files) (length initial-temp-files)))))) + (test-dwim-shell-security-teardown))) + +(provide 'test-dwim-shell-security) +;;; test-dwim-shell-security.el ends here |
