Compare commits

..
Author SHA1 Message Date
dependabot[bot] 7b44bc029b Javascript dependency: Bump serialize-javascript in /web
Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) from 7.0.5 to 7.1.3.
- [Release notes](https://github.com/yahoo/serialize-javascript/releases)
- [Commits](https://github.com/yahoo/serialize-javascript/compare/v7.0.5...v7.1.3)

---
updated-dependencies:
- dependency-name: serialize-javascript
  dependency-version: 7.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:33:14 +00:00
24 changed files with 53 additions and 220 deletions

No files matched your search

+1 -9
View File
@@ -335,15 +335,7 @@ jobs:
# on the bundle, set by SetFile -a E in _create_dmg to hide the .app
# extension in the window, and --strict rejects any xattr at all.
echo "=== notarisation and Gatekeeper ==="
# stapler looks the ticket up in Apple's CloudKit, and a dropped
# connection there fails the step with error 68, so it is retried.
# A genuinely broken ticket still fails all three attempts.
for attempt in 1 2 3; do
xcrun stapler validate "${app}" && break
[ "${attempt}" -lt 3 ] || exit 1
echo "stapler validate failed; retrying in $((attempt * 30))s"
sleep $((attempt * 30))
done
xcrun stapler validate "${app}"
spctl --assess --type execute --verbose=4 "${app}"
echo "=== signature ==="
+17 -24
View File
@@ -50,9 +50,8 @@ on:
"registries" is the other half: it skips the download server
entirely and runs only the container re-tag and the chart push,
so those can be exercised against a candidate tag without a
release. It does not upload to PyPI, so that a rehearsal cannot
publish a candidate's wheel. "pypi" runs only the PyPI upload,
for finishing a promotion whose upload failed.
release. It does not upload to PyPI, there being no published
wheel to upload.
type: choice
required: true
default: release-directory
@@ -61,7 +60,6 @@ on:
- files
- everything
- registries
- pypi
concurrency:
group: 'promote-release'
@@ -126,7 +124,7 @@ jobs:
needs: check
# Skipped entirely when only the registries are being exercised, so a
# rehearsal of those never touches the download server.
if: ${{ inputs.scope != 'registries' && inputs.scope != 'pypi' }}
if: ${{ inputs.scope != 'registries' }}
runs-on: [self-hosted, publish]
environment: publishing
@@ -312,8 +310,7 @@ jobs:
if: >-
${{ always()
&& needs.check.result == 'success'
&& (inputs.scope == 'everything' || inputs.scope == 'registries'
|| inputs.scope == 'pypi')
&& (inputs.scope == 'everything' || inputs.scope == 'registries')
&& (needs.promote.result == 'success'
|| needs.promote.result == 'skipped') }}
runs-on: ubuntu-24.04
@@ -344,7 +341,6 @@ jobs:
# acceptance test ran against. imagetools works registry-side, so
# nothing is pulled or pushed through this runner.
- name: Publish the container
if: ${{ inputs.scope != 'pypi' }}
env:
LOCATION: ${{ inputs.location }}
TAGS: ${{ inputs.container-tags }}
@@ -386,7 +382,6 @@ jobs:
# the image would, oci://docker.io/dpage was an odd home for a chart in
# the first place, and it needs no stored credential here.
- name: Publish the Helm chart
if: ${{ inputs.scope != 'pypi' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTOR: ${{ github.actor }}
@@ -398,35 +393,33 @@ jobs:
helm push pgadmin4-helm-*.tgz oci://ghcr.io/pgadmin-org
helm registry logout ghcr.io
# Fetched from the candidate rather than rebuilt, so what reaches PyPI
# is the file the acceptance test installed. Not from the mirrors: they
# sync from the download server hourly, so the release is not there
# yet when this runs.
# Fetched from the published release rather than rebuilt, so what
# reaches PyPI is the file the acceptance test installed.
- name: Collect the wheel
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
if: ${{ inputs.scope == 'everything' }}
env:
VERSION: ${{ inputs.version }}
LOCATION: ${{ inputs.location }}
run: |
set -euo pipefail
mkdir -p pypi-dist
BASE="https://developer.pgadmin.org/builds/${LOCATION}"
BASE="https://ftp.postgresql.org/pub/pgadmin/pgadmin4/v${VERSION}"
WHEEL="pgadmin4-${VERSION}-py3-none-any.whl"
curl -fsS -o "pypi-dist/${WHEEL}" "${BASE}/${WHEEL}"
curl -fsS -o "pypi-dist/${WHEEL}" "${BASE}/pip/${WHEEL}"
# Beside the wheel rather than in pypi-dist, because everything in
# that directory is uploaded and PyPI has not accepted detached
# signatures since 2023.
curl -fsS -o "${WHEEL}.asc" "${BASE}/${WHEEL}.asc"
curl -fsS -o "${WHEEL}.asc" "${BASE}/pip/${WHEEL}.asc"
ls -l pypi-dist
# The wheel arrives over TLS, which says which server sent it and
# nothing about how it got there, and the signature that has been
# sitting beside the file all along is the only thing that ties these
# bytes back to the signing key. Checking it
# The wheel arrives over TLS, which says it came from the mirror and
# nothing about what the mirror was given: the tree is replicated from
# our download server to machines we do not run, and the signature that
# has been sitting beside the file all along is the only thing that ties
# these bytes back to the key on the publishing server. Checking it
# costs a second and turns the last hop of a release into something an
# attacker cannot quietly alter.
- name: Verify the wheel before uploading it
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
if: ${{ inputs.scope == 'everything' }}
env:
VERSION: ${{ inputs.version }}
run: |
@@ -448,7 +441,7 @@ jobs:
# Trusted publishing: PyPI verifies this workflow's OIDC identity, so
# there is no API token to hold, rotate or leak.
- name: Upload to PyPI
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
if: ${{ inputs.scope == 'everything' }}
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: pypi-dist
-13
View File
@@ -90,13 +90,6 @@ jobs:
container:
needs: location
if: inputs.publish
# The caller caps a called workflow's token: the top-level
# 'contents: read' leaves 'packages: none', so the publish-container job's
# 'packages: write' request must be granted here or the workflow is
# rejected before it starts.
permissions:
contents: read
packages: write
uses: ./.github/workflows/check-container-build.yml
with:
publish: true
@@ -192,16 +185,10 @@ jobs:
steps:
- uses: actions/checkout@v7
# Only the build outputs, which are all named pgadmin4-*. The container
# build also leaves a 'pgadmin-org~pgadmin4~*.dockerbuild' build-record
# artifact that docker/build-push-action uploads; it is not a release
# file, and it is not stored as a plain zip, so download-artifact fails
# to extract it and, downloading everything, fails the whole step.
- name: Collect the build artefacts
uses: actions/download-artifact@v7
with:
path: artefacts
pattern: pgadmin4-*
# The layout is the one the publishing scripts and the download site
# expect, and is what the wrapper's stage-create verb creates on the
+2 -13
View File
@@ -141,21 +141,10 @@ jobs:
echo "=== gatekeeper assessment ==="
spctl --assess --type execute --verbose=4 "${APP}"
echo "=== stapled notarisation ==="
# stapler looks the ticket up in Apple's CloudKit, and a dropped
# connection there fails the step with error 68, so it is retried.
# A genuinely broken ticket still fails all three attempts.
validate() {
for attempt in 1 2 3; do
xcrun stapler validate "$1" && return 0
[ "${attempt}" -lt 3 ] || return 1
echo "stapler validate failed; retrying in $((attempt * 30))s"
sleep $((attempt * 30))
done
}
validate "${APP}"
xcrun stapler validate "${APP}"
for F in dist/*.dmg; do
echo "--- ${F} ---"
validate "${F}"
xcrun stapler validate "${F}"
done
- name: Archive the disk image and zip
+1 -8
View File
@@ -59,15 +59,8 @@ RUN --mount=type=bind,source=.git,target=/pgadmin4/.git \
FROM python:3-alpine AS env-builder
# Install dependencies
#
# libpq is a runtime dependency of psycopg's C extension: psycopg[c] is built
# from source against postgresql-dev, but that is removed with build-deps
# below, so libpq.so.5 must be installed separately to survive. Without it,
# importing psycopg (which the Sphinx docs build does, via config.py) fails
# with "no pq wrapper available".
RUN apk add --no-cache \
make \
libpq && \
make && \
apk add --no-cache --virtual build-deps \
build-base \
openssl-dev \
+2 -9
View File
@@ -563,16 +563,9 @@ REM and some of them are optional.
SETLOCAL EnableDelayedExpansion
SET "COMPONENTS="
REM FOR expands an item containing a wildcard against the current directory
REM and silently drops it when nothing matches there, so the loop must run
REM in the runtime directory for the versioned libraries to be found at
REM all. Plain names are passed through whether or not the file exists,
REM hence the IF EXIST for the optional ones.
PUSHD "%BUILDROOT%\runtime" || EXIT /B 1
FOR %%f IN (pgAdmin4.exe libpq.dll libcrypto-*-x64.dll libssl-*-x64.dll libintl-*.dll libiconv-*.dll liblz4.dll libzstd.dll zlib1.dll pg_dump.exe pg_dumpall.exe pg_restore.exe psql.exe kinit.exe krb5_64.dll comerr64.dll k5sprt64.dll gssapi64.dll krbcc64.dll ccapiserver.exe) DO (
IF EXIST "%%f" SET "COMPONENTS=!COMPONENTS! "%BUILDROOT%\runtime\%%f""
FOR %%p IN (pgAdmin4.exe libpq.dll libcrypto-*-x64.dll libssl-*-x64.dll libintl-*.dll libiconv-*.dll liblz4.dll libzstd.dll zlib1.dll pg_dump.exe pg_dumpall.exe pg_restore.exe psql.exe kinit.exe krb5_64.dll comerr64.dll k5sprt64.dll gssapi64.dll krbcc64.dll ccapiserver.exe) DO (
FOR /F "delims=" %%f IN ('DIR /B "%BUILDROOT%\runtime\%%p" 2^>nul') DO SET "COMPONENTS=!COMPONENTS! "%BUILDROOT%\runtime\%%f""
)
POPD
IF "!COMPONENTS!" == "" (
ECHO.
ECHO ************************************************************
+1 -1
View File
@@ -141,7 +141,7 @@ Bug fixes
| `Issue #10386 <https://github.com/pgadmin-org/pgadmin4/issues/10386>`_ - Stop the Query Tool spinner, and its idle CPU use, whilst a password prompt is open.
| `Issue #10401 <https://github.com/pgadmin-org/pgadmin4/issues/10401>`_ - Show default privileges in the Database properties panel.
| `Issue #10409 <https://github.com/pgadmin-org/pgadmin4/issues/10409>`_ - Include the authenticated user in the access log on logout.
| `Issue #10414 <https://github.com/pgadmin-org/pgadmin4/issues/10414>`_ - Fix the Linux packages shipping a stray copy of Python as ``venv/bin/python``, which stopped the DEB package installing on Ubuntu 26.04.
| `Issue #10414 <https://github.com/pgadmin-org/pgadmin4/issues/10414>`_ - Fix the Linux packages shipping a stray copy of Python as ``venv/bin/𝜋thon``, which stopped the DEB package installing on Ubuntu 26.04.
| `Issue #10415 <https://github.com/pgadmin-org/pgadmin4/issues/10415>`_ - Prefer the user's email address as the Gravatar identifier.
| `Issue #10418 <https://github.com/pgadmin-org/pgadmin4/issues/10418>`_ - Remove the dead ``usr`` parameter from Query Tool connection URLs.
| `Issue #10419 <https://github.com/pgadmin-org/pgadmin4/issues/10419>`_ - Fix Kerberos login failures returning a JSON 500 response, and first-time automatically created Kerberos logins being rejected.
+1 -1
View File
@@ -75,4 +75,4 @@ urllib3==2.8.*; python_version > '3.9'
user-agents==2.2.0
Werkzeug==3.1.*
WTForms==3.1.*; python_version <= '3.9'
WTForms==3.3.*; python_version > '3.9'
WTForms==3.2.*; python_version > '3.9'
+1 -1
View File
@@ -144,7 +144,7 @@
"resolutions": {
"rc-resize-observer": "1.4.0",
"@xmldom/xmldom": "^0.8.15",
"serialize-javascript": "^7.0.5",
"serialize-javascript": "^7.1.3",
"ip-address": "^10.3.1",
"postcss": "^8.5.31",
"ws": "^8.20.1",
@@ -33,11 +33,6 @@ class ViewsGetMsqlTestCase(BaseTestGenerator):
# Load test data
self.data = self.test_data
# Check server type
if "server_type" in self.data and \
self.server['type'] != self.data["server_type"]:
self.skipTest(self.data["skip_msg"])
# Create db connection
self.db_name = parent_node_dict["database"][-1]["db_name"]
schema_info = parent_node_dict["schema"][-1]
@@ -1558,9 +1558,7 @@
"query": "\"CREATE OR REPLACE VIEW %s.%s AS SELECT t.id, concat(t.a, ' x ', t.b) AS ab, (t.d)::text AS ds, u.n FROM (VALUES (1, 'a', 'b', now()::date)) t(id, a, b, d) CROSS JOIN (VALUES (2)) u(n); ALTER TABLE %s.%s OWNER TO %s\" % (schema_name, view_name, schema_name, view_name, server['username'])"
},
"test_data": {
"definition": "SELECT t.id, concat(t.a, ' x ', t.b) AS ab, u.n FROM (VALUES (1, 'a', 'b', now()::date)) t(id, a, b, d) CROSS JOIN (VALUES (2)) u(n)",
"server_type": "pg",
"skip_msg": "EPAS can change a view's columns without dropping it."
"definition": "SELECT t.id, concat(t.a, ' x ', t.b) AS ab, u.n FROM (VALUES (1, 'a', 'b', now()::date)) t(id, a, b, d) CROSS JOIN (VALUES (2)) u(n)"
},
"mocking_required": false,
"mock_data": {},
@@ -1580,9 +1578,7 @@
"query": "\"CREATE OR REPLACE VIEW %s.%s AS SELECT id, valid_from, name FROM (VALUES (1, now()::date, 'a')) t(id, valid_from, name); ALTER TABLE %s.%s OWNER TO %s\" % (schema_name, view_name, schema_name, view_name, server['username'])"
},
"test_data": {
"definition": "SELECT id, valid_from FROM (VALUES (1, now()::date, 'a')) t(id, valid_from, name)",
"server_type": "pg",
"skip_msg": "EPAS can change a view's columns without dropping it."
"definition": "SELECT id, valid_from FROM (VALUES (1, now()::date, 'a')) t(id, valid_from, name)"
},
"mocking_required": false,
"mock_data": {},
@@ -1571,13 +1571,7 @@ class TestGetClosestParentStaysInStorage(_StorageOpsMixin, BaseTestGenerator):
scenarios = [('default', dict())]
def runTest(self):
_closest = Filemanager.get_closest_parent
# The File Manager uses forward-slash paths; normalise the separator
# so the expectations hold on Windows, where os.path.join uses "\".
def closest(*args):
return _closest(*args).replace('\\', '/')
closest = Filemanager.get_closest_parent
os.makedirs(os.path.join(self.storage_dir, "a"))
self.assertEqual(closest(self.storage_dir, "/a/"), "/a/")
self.assertEqual(closest(self.storage_dir, "/a/gone/deeper/"), "/a/")
@@ -43,13 +43,6 @@ class BackupSymlinkRaceTest(BaseTestGenerator):
]
def setUp(self):
# Opening the file without following a leaf symbolic link and handing
# it to the utility as /dev/fd/N needs O_NOFOLLOW and descriptor-based
# opens, which exist only on POSIX; on Windows the real path is passed
# and this protection does not apply.
if os.name == 'nt':
self.skipTest(
'Descriptor-based symlink-race protection is POSIX-only.')
if 'default_binary_paths' not in self.server or \
self.server['default_binary_paths'] is None or \
self.server['type'] not in self.server['default_binary_paths'] or \
@@ -45,13 +45,6 @@ class ImportExportSymlinkRaceTest(BaseTestGenerator):
]
def setUp(self):
# Opening the file without following a leaf symbolic link and handing
# it to the utility as /dev/fd/N needs O_NOFOLLOW and descriptor-based
# opens, which exist only on POSIX; on Windows the real path is passed
# and this protection does not apply.
if os.name == 'nt':
self.skipTest(
'Descriptor-based symlink-race protection is POSIX-only.')
if 'default_binary_paths' not in self.server or \
self.server['default_binary_paths'] is None or \
self.server['type'] not in self.server['default_binary_paths'] or \
@@ -46,13 +46,6 @@ class RestoreSymlinkRaceTest(BaseTestGenerator):
]
def setUp(self):
# Opening the file without following a leaf symbolic link and handing
# it to the utility as /dev/fd/N needs O_NOFOLLOW and descriptor-based
# opens, which exist only on POSIX; on Windows the real path is passed
# and this protection does not apply.
if os.name == 'nt':
self.skipTest(
'Descriptor-based symlink-race protection is POSIX-only.')
if 'default_binary_paths' not in self.server or \
self.server['default_binary_paths'] is None or \
self.server['type'] not in self.server['default_binary_paths'] or \
@@ -25,7 +25,7 @@ from unittest.mock import MagicMock, patch
import psycopg
from psycopg.conninfo import conninfo_to_dict, make_conninfo
from pgadmin.utils import NO_CLIENT_FILE, fs_short_path
from pgadmin.utils import NO_CLIENT_FILE
from pgadmin.utils.route import BaseTestGenerator
import pgadmin.utils.driver.psycopg3 as psycopg3_driver
@@ -106,8 +106,7 @@ class ConnectionStringClientFilesTestCase(_StorageMixin, BaseTestGenerator):
'sslcert': NO_CLIENT_FILE,
'sslkey': NO_CLIENT_FILE})),
('Server mode passes on a configured file that does not exist',
dict(posix_only=True,
params={'sslcert': '/missing.crt', 'sslkey': '/missing.key',
dict(params={'sslcert': '/missing.crt', 'sslkey': '/missing.key',
'sslrootcert': '/missing-root.crt',
'sslcrl': '/missing.crl'},
expected={'passfile': NO_CLIENT_FILE,
@@ -134,25 +133,15 @@ class ConnectionStringClientFilesTestCase(_StorageMixin, BaseTestGenerator):
server_mode = True
expected = {}
absent = ()
posix_only = False
def runTest(self):
# On Windows a configured file that cannot be found is disabled
# (NO_CLIENT_FILE), not passed on as its path; both are unopenable.
if self.posix_only and os.name == 'nt':
self.skipTest(
'A missing configured file is disabled on Windows.')
dsn, display = self._connection_string(self.params, self.server_mode)
for key, value in self.expected.items():
if value not in (NO_CLIENT_FILE, 'system'):
# Expected files are given relative to the storage
# directory, and resolved as get_complete_file_path() returns
# them: the short (8.3) form with forward slashes on Windows,
# unchanged on POSIX.
value = fs_short_path(
os.path.join(self.storage, value)).replace('\\', '/')
# directory.
value = os.path.join(self.storage, value)
self.assertEqual(dsn.get(key), value, key)
for key in self.absent:
self.assertNotIn(key, dsn)
@@ -144,18 +144,7 @@ class ConnectionStringKerberosTestCase(BaseTestGenerator):
shutil.rmtree(self.storage, ignore_errors=True)
def runTest(self):
# Any scenario that puts require_auth into the connection string needs
# a libpq that knows it: create_connection_string() validates the
# string, and a real_parser scenario parses it, with the installed
# libpq, which before 16 rejects require_auth. Only the connection
# parameter version is mocked per scenario, not the installed libpq.
produces_require_auth = (
self.real_parser or
getattr(self, 'error', False) or
'require_auth' in self.expected or
'require_auth' in self.params
)
if produces_require_auth and not LIBPQ_HAS_REQUIRE_AUTH:
if self.real_parser and not LIBPQ_HAS_REQUIRE_AUTH:
self.skipTest('The installed libpq cannot parse require_auth.')
manager = server_manager.ServerManager.__new__(
@@ -228,12 +228,6 @@ class InheritedCredentialsTestCase(_ServiceMixin, BaseTestGenerator):
conn.close()
with patch.dict(os.environ, env):
# The runner's own environment may already hold the other
# inherited credential variable (CI sets PGPASSWORD); drop it so
# the scenario sees only the one it set.
for other in ('PGPASSWORD', 'PGSERVICE'):
if other != self.var:
os.environ.pop(other, None)
removed = remove_inherited_libpq_credentials()
remaining = dict(os.environ)
self.assertEqual(removed, [self.var])
@@ -22,7 +22,6 @@ from unittest.mock import MagicMock, patch
import paramiko
from pgadmin.utils import fs_short_path
from pgadmin.utils.route import BaseTestGenerator
from pgadmin.utils.driver.psycopg3 import server_manager as sm
@@ -205,12 +204,8 @@ class TestSSHTunnelIdentityFileConfinement(
self.assertNotIn(self.alice, result[1])
else:
self.assertEqual(result, (True, None))
# get_complete_file_path() returns the short (8.3) form with
# forward slashes on Windows, unchanged on POSIX.
self.assertEqual(
forwarder.call_args.kwargs['ssh_pkey'],
fs_short_path(
os.path.join(self.alice, 'mykey')).replace('\\', '/'))
self.assertEqual(forwarder.call_args.kwargs['ssh_pkey'],
os.path.join(self.alice, 'mykey'))
class TestSSHTunnelRealSSHTunnelForwarder(
@@ -26,19 +26,12 @@ import flask
import config
from pgadmin.utils import (
connection_params_to_env, fs_short_path, get_complete_file_path)
connection_params_to_env, get_complete_file_path)
from pgadmin.utils.route import BaseTestGenerator
import pgadmin.utils.driver.psycopg3.server_manager as server_manager
def _canon(path):
"""Compare paths as get_complete_file_path() returns them: on Windows the
short (8.3) form libpq is given, so an expected long path matches;
fs_short_path() is the identity on POSIX."""
return os.path.normpath(fs_short_path(path))
class _StorageMixin:
"""Builds a storage root holding two users and a shared directory."""
@@ -145,7 +138,8 @@ class CompleteFilePathContainmentTestCase(_StorageMixin, BaseTestGenerator):
else:
# Expected paths are given relative to the temporary root.
expected = os.path.join(self.outer, *self.expected.split('/'))
self.assertEqual(_canon(result), _canon(expected))
self.assertEqual(os.path.normpath(result),
os.path.normpath(expected))
class CompleteFilePathDesktopTestCase(_StorageMixin, BaseTestGenerator):
@@ -269,8 +263,8 @@ class CompleteFilePathRealStorageDirTestCase(_StorageMixin,
def runTest(self):
self.assertEqual(
_canon(self._resolve_real('/.pgpass')),
_canon(os.path.join(self.alice, '.pgpass')))
os.path.normpath(self._resolve_real('/.pgpass')),
os.path.join(self.alice, '.pgpass'))
with self.assertRaises(PermissionError) as cm:
self._resolve_real('../bob/.pgpass')
@@ -278,8 +272,8 @@ class CompleteFilePathRealStorageDirTestCase(_StorageMixin,
str(cm.exception))
self.assertEqual(
_canon(self._resolve_real('Team:/.pgpass')),
_canon(os.path.join(self.shared, '.pgpass')))
os.path.normpath(self._resolve_real('Team:/.pgpass')),
os.path.join(self.shared, '.pgpass'))
# A name that is not configured is just part of a relative path.
self.assertIsNone(self._resolve_real('Nope:/.pgpass'))
@@ -411,8 +405,8 @@ class CompleteFilePathWriteTargetTestCase(_StorageMixin, BaseTestGenerator):
else:
result = get_complete_file_path('Team:/keylog', validate=False,
for_write=True)
self.assertEqual(_canon(result),
_canon(os.path.join(self.shared, 'keylog')))
self.assertEqual(os.path.normpath(result),
os.path.join(self.shared, 'keylog'))
def _user(is_admin):
@@ -19,7 +19,6 @@ from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from flask import session
import psycopg
from psycopg.conninfo import make_conninfo, conninfo_to_dict
import config
@@ -33,11 +32,6 @@ from pgadmin.utils.route import BaseTestGenerator
USER_CACHE = 'FILE:/tmp/pgadmin_cache_alice'
SERVICE_ACCOUNT = {'KRB5CCNAME': 'FILE:/tmp/krb5cc_service_account'}
# The libpq that parses a connection string is the one installed, which may
# be older than 16 and then rejects require_auth. Scenarios that build or
# parse a string carrying require_auth with the real libpq need 16 or later.
LIBPQ_HAS_REQUIRE_AUTH = psycopg.pq.version() >= 160000
def _login(source, ccache=USER_CACHE):
session['auth_source_manager'] = {'current_source': source}
@@ -569,9 +563,6 @@ class ApplyKerberosConnParamsKeyInjectionTestCase(BaseTestGenerator):
def setUp(self):
unittest.TestCase.setUp(self)
# The real libpq builds and parses the string carrying require_auth.
if not LIBPQ_HAS_REQUIRE_AUTH:
self.skipTest('The installed libpq cannot parse require_auth.')
def runTest(self):
params = dict(self.params)
@@ -635,9 +626,6 @@ class ConnectionStringKerberosKeyInjectionTestCase(BaseTestGenerator):
def setUp(self):
unittest.TestCase.setUp(self)
# The real libpq builds and parses the string carrying require_auth.
if not LIBPQ_HAS_REQUIRE_AUTH:
self.skipTest('The installed libpq cannot parse require_auth.')
def runTest(self):
import pgadmin.utils.driver.psycopg3.server_manager as server_manager
@@ -23,7 +23,7 @@ from unittest.mock import patch
from psycopg.conninfo import conninfo_to_dict
from pgadmin.utils import (
utility_conninfo, connection_params_to_env, fs_short_path, NO_CLIENT_FILE)
utility_conninfo, connection_params_to_env, NO_CLIENT_FILE)
from pgadmin.utils.route import BaseTestGenerator
CREDENTIAL_FILES = ('passfile', 'sslcert', 'sslkey')
@@ -55,7 +55,7 @@ class UtilityConninfoTestCase(BaseTestGenerator):
'sslkey': NO_CLIENT_FILE})),
('Server mode, a configured file that does not exist is passed on '
'as the env translation gives it',
dict(server_mode=True, database='postgres', posix_only=True,
dict(server_mode=True, database='postgres',
params={'sslcert': 'missing.crt'},
expected={'dbname': 'postgres',
'passfile': NO_CLIENT_FILE,
@@ -129,23 +129,8 @@ class UtilityConninfoTestCase(BaseTestGenerator):
for p in reversed(patches):
p.stop()
# On Windows a configured file that cannot be found is disabled
# (NO_CLIENT_FILE), not passed through as its resolved path; both are
# unopenable, so the connection still cannot use another file.
if getattr(self, 'posix_only', False) and os.name == 'nt':
self.skipTest(
'A missing configured file is disabled on Windows.')
def _exp(v):
# Storage files resolve as get_complete_file_path() returns them:
# the short (8.3) form with forward slashes on Windows, unchanged
# on POSIX.
if isinstance(v, str) and '<storage>' in v:
return fs_short_path(
v.replace('<storage>', self.storage)).replace('\\', '/')
return v
expected = {k: _exp(v) for k, v in self.expected.items()}
expected = {k: v.replace('<storage>', self.storage)
for k, v in self.expected.items()}
self.assertEqual(result, expected)
@@ -28,21 +28,13 @@ from psycopg.conninfo import conninfo_to_dict
from pgadmin.misc.bgprocess.processes import BatchProcess
from pgadmin.utils import (
NO_CLIENT_FILE, database_conninfo, fs_short_path,
utility_connection_params)
NO_CLIENT_FILE, database_conninfo, utility_connection_params)
from pgadmin.utils.route import BaseTestGenerator
DATABASE = 'host=evil dbname=x'
CONNECTION_PARAMS = {'passfile': '.pgpass'}
def _resolved(path):
"""A storage file path as get_complete_file_path() returns it: on
Windows the short (8.3) form with forward slashes, which libpq is given;
unchanged on POSIX, where fs_short_path() is the identity."""
return fs_short_path(path).replace('\\', '/')
SCENARIOS = [
('Server mode', dict(server_mode=True)),
('Desktop mode', dict(server_mode=False)),
@@ -113,7 +105,7 @@ class _CallSiteBase:
if not self.server_mode:
return {'dbname': DATABASE}
return {'dbname': DATABASE,
'passfile': _resolved(os.path.join(self.storage, '.pgpass')),
'passfile': os.path.join(self.storage, '.pgpass'),
'sslcert': NO_CLIENT_FILE, 'sslkey': NO_CLIENT_FILE}
def _assert_dbname(self, args, expected=None):
@@ -207,7 +199,7 @@ class ConnectionParamsSourceTestCase(_CallSiteBase, BaseTestGenerator):
lambda: utility_conninfo(
'postgres', utility_connection_params(manager, server)))
self.assertEqual(conninfo_to_dict(conninfo)['passfile'],
_resolved(os.path.join(self.storage, '.pgpass')))
os.path.join(self.storage, '.pgpass'))
class RestoreArgsTestCase(_CallSiteBase, BaseTestGenerator):
@@ -304,11 +304,7 @@ class PgDumpallMaintenanceDatabaseTestCase(BaseTestGenerator):
f.write('[pgadmin_probe]\npassfile={0}\n'.format(
service_passfile))
# The raw (control) value is parsed by libpq as a connection string,
# where a backslash escapes the next character; a Windows path must
# use forward slashes, which libpq accepts, to survive it unchanged.
hostile = 'dbname=postgres passfile={0}'.format(
service_passfile.replace(os.sep, '/'))
hostile = 'dbname=postgres passfile={0}'.format(service_passfile)
database = database_conninfo(hostile) if self.fixed else hostile
dbname, server_env = self._utility_dbname()
env = {k: v for k, v in os.environ.items() if not k.startswith('PG')}