From a3ce790f8bc4999fa4c2e288a47510977b445e61 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Wed, 19 Aug 2026 13:21:11 +0100 Subject: [PATCH] fix: guard PasswordExec against None host/port/username Service-only connections (pg_service.conf) leave host, port and username unset, so a per-server Password Exec Command crashed in get() with TypeError: replace() argument 2 must be str, not None. Fall back to an empty string for each unset placeholder instead. Closes #10237 --- web/pgadmin/utils/passexec.py | 10 +++-- web/pgadmin/utils/tests/test_passexec.py | 49 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 web/pgadmin/utils/tests/test_passexec.py diff --git a/web/pgadmin/utils/passexec.py b/web/pgadmin/utils/passexec.py index 24a05d50bf7..6a35312c671 100644 --- a/web/pgadmin/utils/passexec.py +++ b/web/pgadmin/utils/passexec.py @@ -39,10 +39,12 @@ def get(self): raise NotImplementedError('Passexec not available in server mode') driver = get_driver(config.PG_DEFAULT_DRIVER) self.cmd = str(self.cmd) - self.cmd = self.cmd.replace('%HOSTNAME%', self.host) - self.cmd = self.cmd.replace('%PORT%', str(self.port)) - self.cmd = self.cmd.replace('%USERNAME%', - driver.qtIdent(None,self.username)) + self.cmd = self.cmd.replace('%HOSTNAME%', self.host or '') + self.cmd = self.cmd.replace( + '%PORT%', str(self.port) if self.port is not None else '') + self.cmd = self.cmd.replace( + '%USERNAME%', + driver.qtIdent(None, self.username) if self.username else '') with self.lock: if not self.password or self.is_expired(): if not self.cmd: diff --git a/web/pgadmin/utils/tests/test_passexec.py b/web/pgadmin/utils/tests/test_passexec.py new file mode 100644 index 00000000000..78880e9c370 --- /dev/null +++ b/web/pgadmin/utils/tests/test_passexec.py @@ -0,0 +1,49 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Regression test for PasswordExec.get() with a Service-only connection. + +When a server is configured via the ``Service`` field (pg_service.conf) with +host, port and username left blank, ``PasswordExec`` is constructed with +those attributes as ``None``. ``get()`` used to pass them straight into +``str.replace()``, which raises ``TypeError: replace() argument 2 must be +str, not None``. The fix substitutes an empty string for any unset +placeholder instead.""" + +from unittest.mock import patch, MagicMock + +from pgadmin.utils.route import BaseTestGenerator +from pgadmin.utils.passexec import PasswordExec + + +class TestPasswordExecServiceOnly(BaseTestGenerator): + """PasswordExec.get() must not raise when host/port/username are None.""" + + def setUp(self): + pass + + def runTest(self): + pexec = PasswordExec( + 'echo %HOSTNAME%:%PORT%:%USERNAME%', None, None, None) + + fake_driver = MagicMock() + fake_driver.qtIdent.return_value = 'quoted' + fake_proc = MagicMock(stdout='secret\n') + + with self.app.app_context(), \ + patch('pgadmin.utils.passexec.get_driver', + return_value=fake_driver), \ + patch('pgadmin.utils.passexec.subprocess.run', + return_value=fake_proc) as mock_run, \ + patch('pgadmin.utils.passexec.config.SERVER_MODE', False): + password = pexec.get() + + self.assertEqual(password, 'secret') + self.assertEqual(mock_run.call_args[0][0], 'echo ::') + fake_driver.qtIdent.assert_not_called()