]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Resolves gpg-agent hanging around and using inotify handles too (#11848)
authorTrenton H <797416+stumpylog@users.noreply.github.com>
Wed, 21 Jan 2026 23:53:54 +0000 (15:53 -0800)
committerGitHub <noreply@github.com>
Wed, 21 Jan 2026 23:53:54 +0000 (15:53 -0800)
src/paperless_mail/tests/test_preprocessor.py

index 90df77ba8a4db1841d0d640ea628e0fd261c30f6..2ad9410f9295f50edb297dfe6596058fbf580ad9 100644 (file)
@@ -1,5 +1,7 @@
 import email
 import email.contentmanager
+import shutil
+import subprocess
 import tempfile
 from email.message import Message
 from email.mime.application import MIMEApplication
@@ -34,6 +36,30 @@ class MessageEncryptor:
         )
         self.gpg.gen_key(input_data)
 
+    def cleanup(self) -> None:
+        """
+        Kill the gpg-agent process and clean up the temporary GPG home directory.
+
+        This uses gpgconf to properly terminate the agent, which is the officially
+        recommended cleanup method from the GnuPG project. python-gnupg does not
+        provide built-in cleanup methods as it's only a wrapper around the gpg CLI.
+        """
+        # Kill the gpg-agent using the official GnuPG cleanup tool
+        try:
+            subprocess.run(
+                ["gpgconf", "--kill", "gpg-agent"],
+                env={"GNUPGHOME": self.gpg_home},
+                check=False,
+                capture_output=True,
+                timeout=5,
+            )
+        except (FileNotFoundError, subprocess.TimeoutExpired):
+            # gpgconf not found or hung - agent will timeout eventually
+            pass
+
+        # Clean up the temporary directory
+        shutil.rmtree(self.gpg_home, ignore_errors=True)
+
     @staticmethod
     def get_email_body_without_headers(email_message: Message) -> bytes:
         """
@@ -85,8 +111,20 @@ class MessageEncryptor:
 
 
 class TestMailMessageGpgDecryptor(TestMail):
+    @classmethod
+    def setUpClass(cls):
+        """Create GPG encryptor once for all tests in this class."""
+        super().setUpClass()
+        cls.messageEncryptor = MessageEncryptor()
+
+    @classmethod
+    def tearDownClass(cls):
+        """Clean up GPG resources after all tests complete."""
+        if hasattr(cls, "messageEncryptor"):
+            cls.messageEncryptor.cleanup()
+        super().tearDownClass()
+
     def setUp(self):
-        self.messageEncryptor = MessageEncryptor()
         with override_settings(
             EMAIL_GNUPG_HOME=self.messageEncryptor.gpg_home,
             EMAIL_ENABLE_GPG_DECRYPTOR=True,
@@ -138,13 +176,28 @@ class TestMailMessageGpgDecryptor(TestMail):
 
     def test_decrypt_fails(self):
         encrypted_message, _ = self.create_encrypted_unencrypted_message_pair()
+        # This test creates its own empty GPG home to test decryption failure
         empty_gpg_home = tempfile.mkdtemp()
-        with override_settings(
-            EMAIL_ENABLE_GPG_DECRYPTOR=True,
-            EMAIL_GNUPG_HOME=empty_gpg_home,
-        ):
-            message_decryptor = MailMessageDecryptor()
-            self.assertRaises(Exception, message_decryptor.run, encrypted_message)
+        try:
+            with override_settings(
+                EMAIL_ENABLE_GPG_DECRYPTOR=True,
+                EMAIL_GNUPG_HOME=empty_gpg_home,
+            ):
+                message_decryptor = MailMessageDecryptor()
+                self.assertRaises(Exception, message_decryptor.run, encrypted_message)
+        finally:
+            # Clean up the temporary GPG home used only by this test
+            try:
+                subprocess.run(
+                    ["gpgconf", "--kill", "gpg-agent"],
+                    env={"GNUPGHOME": empty_gpg_home},
+                    check=False,
+                    capture_output=True,
+                    timeout=5,
+                )
+            except (FileNotFoundError, subprocess.TimeoutExpired):
+                pass
+            shutil.rmtree(empty_gpg_home, ignore_errors=True)
 
     def test_decrypt_encrypted_mail(self):
         """