From 3daea9ca57dcf0e6c4a31db4d23f3a7112dc8e82 Mon Sep 17 00:00:00 2001 From: Fook <8185900+fooklook@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:48:50 +0800 Subject: [PATCH] Preserve spaces in connection URL passwords --- CHANGELOG.rst | 5 +++++ pgsync/urls.py | 8 ++++---- tests/test_urls.py | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3a620320..0f6c91fc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,11 @@ Changelog ========= +Unreleased +---------- + +* Preserve spaces in database, search, and Redis connection URL passwords. + 1.0.1 (2020-15-01) ------------------ diff --git a/pgsync/urls.py b/pgsync/urls.py index 70780505..3c9775ae 100644 --- a/pgsync/urls.py +++ b/pgsync/urls.py @@ -92,7 +92,7 @@ def get_search_url( auth: str = "" if user and password: - auth = f"{user}:{quote_plus(password)}@" + auth = f"{user}:{quote(password, safe='')}@" else: logger.debug("Connecting to Search without password.") @@ -142,7 +142,7 @@ def get_database_url( ) ) - auth: str = f"{user}:{quote_plus(password)}" if password else user + auth: str = f"{user}:{quote(password, safe='')}" if password else user if not password: logger.debug("Connecting to database without password.") @@ -193,10 +193,10 @@ def get_redis_url( auth: str = "" if username and password: - auth = f"{quote_plus(username)}:{quote_plus(password)}@" + auth = f"{quote_plus(username)}:{quote(password, safe='')}@" logger.debug("Connecting to Redis with custom username and password.") elif password: - auth = f":{quote_plus(password)}@" + auth = f":{quote(password, safe='')}@" logger.debug("Connecting to Redis with default password.") else: logger.debug("Connecting to Redis without password.") diff --git a/tests/test_urls.py b/tests/test_urls.py index d97787d7..8b72daae 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -1,5 +1,7 @@ """URLS tests.""" +from urllib.parse import unquote, urlsplit + import pytest from mock import MagicMock, patch @@ -16,6 +18,30 @@ class TestUrls(object): """URLS tests.""" + @pytest.mark.parametrize( + "service", ["database", "search", "redis", "redis-default-user"] + ) + @pytest.mark.parametrize( + "password", ["word with spaces", "plus+ @:/?#%", "päss 密码"] + ) + def test_connection_password_round_trip( + self, monkeypatch, service, password + ): + monkeypatch.setattr("pgsync.urls._get_auth", lambda key: None) + for variable in ("PG_URL", "ELASTICSEARCH_URL", "REDIS_URL"): + monkeypatch.setattr(f"pgsync.urls.{variable}", None) + + if service == "database": + url = get_database_url("mydb", user="studio", password=password) + elif service == "search": + url = get_search_url(user="studio", password=password) + elif service == "redis": + url = get_redis_url(username="studio", password=password) + else: + url = get_redis_url(password=password) + + assert unquote(urlsplit(url).password) == password + @patch("pgsync.urls.logger") def test_get_search_url(self, mock_logger): assert (