From 4cad3d740695448ca30c0f9e8d29b715f7d64ee6 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 26 Mar 2025 14:56:31 -0400 Subject: [PATCH 01/22] PYTHON-4575 Remove hostname length check --- pymongo/asynchronous/srv_resolver.py | 2 -- pymongo/synchronous/srv_resolver.py | 2 -- test/test_uri_parser.py | 6 ++++++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 8b811e5dc2..cedd0749e9 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -96,8 +96,6 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) - if self.__slen < 2: - raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) async def get_options(self) -> Optional[str]: from dns import resolver diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index 1b36efd1c9..afe5b23a93 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -96,8 +96,6 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) - if self.__slen < 2: - raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) def get_options(self) -> Optional[str]: from dns import resolver diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 0baefa0c3a..d96cad6b6a 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -24,6 +24,7 @@ sys.path[0:0] = [""] from test import unittest +from unittest.mock import patch from bson.binary import JAVA_LEGACY from pymongo import ReadPreference @@ -553,6 +554,11 @@ def test_port_with_whitespace(self): with self.assertRaisesRegex(ValueError, r"Port contains whitespace character: '\\n'"): parse_uri("mongodb://localhost:27\n017") + def test_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): + with patch("dns.resolver.resolve"): + parse_uri("mongodb+srv://localhost/") + parse_uri("mongodb+srv://mongo.local/") + if __name__ == "__main__": unittest.main() From 118810e30e6fd7fdf5c3df15e3e63f5ed01369be Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 26 Mar 2025 15:10:17 -0400 Subject: [PATCH 02/22] PYTHON-4575 Remove hostname length check --- test/asynchronous/test_dns.py | 6 ------ test/test_dns.py | 6 ------ 2 files changed, 12 deletions(-) diff --git a/test/asynchronous/test_dns.py b/test/asynchronous/test_dns.py index d0e801e123..7316b4df9a 100644 --- a/test/asynchronous/test_dns.py +++ b/test/asynchronous/test_dns.py @@ -186,12 +186,6 @@ def create_tests(cls): class TestParsingErrors(AsyncPyMongoTestCase): async def test_invalid_host(self): - with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb is not"): - client = self.simple_client("mongodb+srv://mongodb") - await client.aconnect() - with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb.com is not"): - client = self.simple_client("mongodb+srv://mongodb.com") - await client.aconnect() with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: an IP address is not"): client = self.simple_client("mongodb+srv://127.0.0.1") await client.aconnect() diff --git a/test/test_dns.py b/test/test_dns.py index 0290eb16d9..c478e481f6 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -184,12 +184,6 @@ def create_tests(cls): class TestParsingErrors(PyMongoTestCase): def test_invalid_host(self): - with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb is not"): - client = self.simple_client("mongodb+srv://mongodb") - client._connect() - with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb.com is not"): - client = self.simple_client("mongodb+srv://mongodb.com") - client._connect() with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: an IP address is not"): client = self.simple_client("mongodb+srv://127.0.0.1") client._connect() From 426d0618488c1bf3b78a7d21cbe78fb539b3eff2 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 30 Mar 2025 22:30:51 -0400 Subject: [PATCH 03/22] PYTHON-4575 Remove hostname length check - Add test --- test/test_uri_parser.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index d96cad6b6a..735c7e863d 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -24,7 +24,7 @@ sys.path[0:0] = [""] from test import unittest -from unittest.mock import patch +from unittest.mock import MagicMock, patch from bson.binary import JAVA_LEGACY from pymongo import ReadPreference @@ -559,6 +559,42 @@ def test_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): parse_uri("mongodb+srv://localhost/") parse_uri("mongodb+srv://mongo.local/") + def test_error_when_return_address_does_not_end_with_srv_domain(self): + test_cases = [ + { + "query": "_mongodb._tcp.localhost", + "mock_target": "localhost.mongodb", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "blogs.evil.com", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongo.local", + "mock_target": "test_1.evil.com", + "expected_error": "Invalid SRV host", + }, + ] + for case in test_cases: + with patch("dns.resolver.resolve") as mock_resolver: + + def mock_resolve(query, record_type, *args, **kwargs): + mock_srv = MagicMock() + mock_srv.target.to_text.return_value = case["mock_target"] + return [mock_srv] + + mock_resolver.side_effect = mock_resolve + domain = case["query"].split("._tcp.")[1] + connection_string = f"mongodb+srv://{domain}" + try: + parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") + if __name__ == "__main__": unittest.main() From 0cbc00f91e1c53bb1f610a1dd98bfe679bfe5e3a Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Mon, 31 Mar 2025 19:08:41 -0400 Subject: [PATCH 04/22] PYTHON-4575 Remove hostname length check - Add test --- pymongo/asynchronous/srv_resolver.py | 4 ++++ pymongo/synchronous/srv_resolver.py | 4 ++++ test/test_uri_parser.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index cedd0749e9..7d6edc46ad 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -96,6 +96,10 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) + if fqdn == self._resolve_uri(True)[0].target.to_text(): + raise ConfigurationError( + "Invalid SRV host: return address is identical to SRV hostname" + ) async def get_options(self) -> Optional[str]: from dns import resolver diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index afe5b23a93..b78f210c68 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -96,6 +96,10 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) + if fqdn == self._resolve_uri(True)[0].target.to_text(): + raise ConfigurationError( + "Invalid SRV host: return address is identical to SRV hostname" + ) def get_options(self) -> Optional[str]: from dns import resolver diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 735c7e863d..7c0fbcb6eb 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -576,6 +576,21 @@ def test_error_when_return_address_does_not_end_with_srv_domain(self): "mock_target": "test_1.evil.com", "expected_error": "Invalid SRV host", }, + { + "query": "_mongodb._tcp.localhost", + "mock_target": "localhost", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.localhost", + "mock_target": "localhost", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.mongo.local", + "mock_target": "mongo.local", + "expected_error": "Invalid SRV host", + }, ] for case in test_cases: with patch("dns.resolver.resolve") as mock_resolver: From 19d31c79443a3f4350d92d040a53fb782f4775d0 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Tue, 1 Apr 2025 18:15:14 -0400 Subject: [PATCH 05/22] Move IO out of __init__ --- pymongo/asynchronous/srv_resolver.py | 5 +++++ pymongo/synchronous/srv_resolver.py | 5 +++++ test/test_uri_parser.py | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 7d6edc46ad..5d8f1540aa 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -133,6 +133,11 @@ async def _get_srv_response_and_hosts( ) -> tuple[resolver.Answer, list[tuple[str, Any]]]: results = await self._resolve_uri(encapsulate_errors) + if self.__fqdn == results[0].target.to_text(): + raise ConfigurationError( + "Invalid SRV host: return address is identical to SRV hostname" + ) + # Construct address tuples nodes = [ (maybe_decode(res.target.to_text(omit_final_dot=True)), res.port) # type: ignore[attr-defined] diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index b78f210c68..36c95c02d3 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -133,6 +133,11 @@ def _get_srv_response_and_hosts( ) -> tuple[resolver.Answer, list[tuple[str, Any]]]: results = self._resolve_uri(encapsulate_errors) + if self.__fqdn == results[0].target.to_text(): + raise ConfigurationError( + "Invalid SRV host: return address is identical to SRV hostname" + ) + # Construct address tuples nodes = [ (maybe_decode(res.target.to_text(omit_final_dot=True)), res.port) # type: ignore[attr-defined] diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 7c0fbcb6eb..e66434103d 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -588,7 +588,7 @@ def test_error_when_return_address_does_not_end_with_srv_domain(self): }, { "query": "_mongodb._tcp.mongo.local", - "mock_target": "mongo.local", + "mock_target": "foo.mongo.local", "expected_error": "Invalid SRV host", }, ] From 9aa3781f3bcbf5b4326b2d385c85079a618ee752 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Tue, 1 Apr 2025 18:26:56 -0400 Subject: [PATCH 06/22] Remove duplicate test --- test/test_uri_parser.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index e66434103d..5c72c024fc 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -581,11 +581,6 @@ def test_error_when_return_address_does_not_end_with_srv_domain(self): "mock_target": "localhost", "expected_error": "Invalid SRV host", }, - { - "query": "_mongodb._tcp.localhost", - "mock_target": "localhost", - "expected_error": "Invalid SRV host", - }, { "query": "_mongodb._tcp.mongo.local", "mock_target": "foo.mongo.local", From e756efce31a87521dbb8428d98a5f06177533a29 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Tue, 1 Apr 2025 18:31:25 -0400 Subject: [PATCH 07/22] Add remaining tests, expected to fail until fixed --- test/test_uri_parser.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 5c72c024fc..a5d49e842d 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -583,7 +583,22 @@ def test_error_when_return_address_does_not_end_with_srv_domain(self): }, { "query": "_mongodb._tcp.mongo.local", - "mock_target": "foo.mongo.local", + "mock_target": "mongo.local", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.localhost", + "mock_target": "test_1.cluster_1localhost", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.mongo.local", + "mock_target": "test_1.my_hostmongo.local", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "cluster.testmongodb.com", "expected_error": "Invalid SRV host", }, ] From abd9eb7973858ee19424ca5fb12d13cc93959a7f Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Tue, 1 Apr 2025 18:50:54 -0400 Subject: [PATCH 08/22] Moved to _get_srv_response_and_hosts --- pymongo/asynchronous/srv_resolver.py | 4 ---- pymongo/synchronous/srv_resolver.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 5d8f1540aa..c5587fb0bf 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -96,10 +96,6 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) - if fqdn == self._resolve_uri(True)[0].target.to_text(): - raise ConfigurationError( - "Invalid SRV host: return address is identical to SRV hostname" - ) async def get_options(self) -> Optional[str]: from dns import resolver diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index 36c95c02d3..ec8f48bf9f 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -96,10 +96,6 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) - if fqdn == self._resolve_uri(True)[0].target.to_text(): - raise ConfigurationError( - "Invalid SRV host: return address is identical to SRV hostname" - ) def get_options(self) -> Optional[str]: from dns import resolver From c7f4fef3740436e273be5e0238c59af73a5cc9ae Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 2 Apr 2025 13:45:40 -0400 Subject: [PATCH 09/22] Separate and rename tests to match prose --- test/test_uri_parser.py | 50 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index a5d49e842d..c390b7c2bd 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -554,12 +554,15 @@ def test_port_with_whitespace(self): with self.assertRaisesRegex(ValueError, r"Port contains whitespace character: '\\n'"): parse_uri("mongodb://localhost:27\n017") - def test_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): + # Initial DNS Seedlist Discovery prose tests + # https://github.com/mongodb/specifications/blob/0a7a8b5/source/initial-dns-seedlist-discovery/tests/README.md#prose-tests + + def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): with patch("dns.resolver.resolve"): parse_uri("mongodb+srv://localhost/") parse_uri("mongodb+srv://mongo.local/") - def test_error_when_return_address_does_not_end_with_srv_domain(self): + def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): test_cases = [ { "query": "_mongodb._tcp.localhost", @@ -576,6 +579,27 @@ def test_error_when_return_address_does_not_end_with_srv_domain(self): "mock_target": "test_1.evil.com", "expected_error": "Invalid SRV host", }, + ] + for case in test_cases: + with patch("dns.resolver.resolve") as mock_resolver: + + def mock_resolve(query, record_type, *args, **kwargs): + mock_srv = MagicMock() + mock_srv.target.to_text.return_value = case["mock_target"] + return [mock_srv] + + mock_resolver.side_effect = mock_resolve + domain = case["query"].split("._tcp.")[1] + connection_string = f"mongodb+srv://{domain}" + try: + parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") + + def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): + test_cases = [ { "query": "_mongodb._tcp.localhost", "mock_target": "localhost", @@ -586,6 +610,28 @@ def test_error_when_return_address_does_not_end_with_srv_domain(self): "mock_target": "mongo.local", "expected_error": "Invalid SRV host", }, + ] + for case in test_cases: + with patch("dns.resolver.resolve") as mock_resolver: + + def mock_resolve(query, record_type, *args, **kwargs): + mock_srv = MagicMock() + mock_srv.target.to_text.return_value = case["mock_target"] + return [mock_srv] + + mock_resolver.side_effect = mock_resolve + domain = case["query"].split("._tcp.")[1] + connection_string = f"mongodb+srv://{domain}" + try: + parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") + + def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain(self): + test_cases = [ + { "query": "_mongodb._tcp.localhost", "mock_target": "test_1.cluster_1localhost", From 577cd7f5e28dadf595c767debe59d75570ef7ac2 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 2 Apr 2025 15:03:09 -0400 Subject: [PATCH 10/22] No more slicing fqdn to get parts --- pymongo/asynchronous/srv_resolver.py | 2 +- pymongo/synchronous/srv_resolver.py | 2 +- test/test_uri_parser.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index c5587fb0bf..9bcedbeb15 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -92,7 +92,7 @@ def __init__( pass try: - self.__plist = self.__fqdn.split(".")[1:] + self.__plist = self.__fqdn.split(".") except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index ec8f48bf9f..dc2c21abca 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -92,7 +92,7 @@ def __init__( pass try: - self.__plist = self.__fqdn.split(".")[1:] + self.__plist = self.__fqdn.split(".") except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index c390b7c2bd..f37c0b4e96 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -629,9 +629,10 @@ def mock_resolve(query, record_type, *args, **kwargs): else: self.fail(f"ConfigurationError was not raised for query: {case['query']}") - def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain(self): + def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain( + self + ): test_cases = [ - { "query": "_mongodb._tcp.localhost", "mock_target": "test_1.cluster_1localhost", From 361b0fa8e85cf276f617ec175aad183fad53df01 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 2 Apr 2025 15:24:16 -0400 Subject: [PATCH 11/22] Handle all srv domains not just < 3 parts --- pymongo/asynchronous/srv_resolver.py | 6 +++++- pymongo/synchronous/srv_resolver.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 9bcedbeb15..4885a48bff 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -92,7 +92,11 @@ def __init__( pass try: - self.__plist = self.__fqdn.split(".") + self.__plist = ( + self.__fqdn.split(".")[1:] + if len(self.__fqdn.split(".")) > 2 + else self.__fqdn.split(".") + ) except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index dc2c21abca..771924a320 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -92,7 +92,11 @@ def __init__( pass try: - self.__plist = self.__fqdn.split(".") + self.__plist = ( + self.__fqdn.split(".")[1:] + if len(self.__fqdn.split(".")) > 2 + else self.__fqdn.split(".") + ) except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) From 4b34415fa527f88ceea965dd113ea186afffb597 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 2 Apr 2025 15:32:37 -0400 Subject: [PATCH 12/22] Update changelog --- doc/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index 0633049857..bc567dabc6 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -19,6 +19,7 @@ PyMongo 4.12 brings a number of changes including: :class:`~pymongo.read_preferences.Secondary`, :class:`~pymongo.read_preferences.SecondaryPreferred`, :class:`~pymongo.read_preferences.Nearest`. Support for ``hedge`` will be removed in PyMongo 5.0. +- Allow valid SRV hostnames with less than 3 parts. Issues Resolved ............... From 5c823fcd4037e68a8b46b286f9fc0e7f030c483e Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 2 Apr 2025 16:41:37 -0400 Subject: [PATCH 13/22] Refactor --- test/test_uri_parser.py | 74 +++++++++++++---------------------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index f37c0b4e96..33ee0bc12c 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -37,6 +37,26 @@ ) +def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): + for case in test_cases: + with patch("dns.resolver.resolve") as mock_resolver: + + def mock_resolve(query, record_type, *args, **kwargs): + mock_srv = MagicMock() + mock_srv.target.to_text.return_value = case["mock_target"] + return [mock_srv] + + mock_resolver.side_effect = mock_resolve + domain = case["query"].split("._tcp.")[1] + connection_string = f"mongodb+srv://{domain}" + try: + parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") + + class TestURI(unittest.TestCase): def test_validate_userinfo(self): self.assertRaises(InvalidURI, parse_userinfo, "foo@") @@ -580,23 +600,7 @@ def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): "expected_error": "Invalid SRV host", }, ] - for case in test_cases: - with patch("dns.resolver.resolve") as mock_resolver: - - def mock_resolve(query, record_type, *args, **kwargs): - mock_srv = MagicMock() - mock_srv.target.to_text.return_value = case["mock_target"] - return [mock_srv] - - mock_resolver.side_effect = mock_resolve - domain = case["query"].split("._tcp.")[1] - connection_string = f"mongodb+srv://{domain}" - try: - parse_uri(connection_string) - except ConfigurationError as e: - self.assertIn(case["expected_error"], str(e)) - else: - self.fail(f"ConfigurationError was not raised for query: {case['query']}") + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): test_cases = [ @@ -611,23 +615,7 @@ def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): "expected_error": "Invalid SRV host", }, ] - for case in test_cases: - with patch("dns.resolver.resolve") as mock_resolver: - - def mock_resolve(query, record_type, *args, **kwargs): - mock_srv = MagicMock() - mock_srv.target.to_text.return_value = case["mock_target"] - return [mock_srv] - - mock_resolver.side_effect = mock_resolve - domain = case["query"].split("._tcp.")[1] - connection_string = f"mongodb+srv://{domain}" - try: - parse_uri(connection_string) - except ConfigurationError as e: - self.assertIn(case["expected_error"], str(e)) - else: - self.fail(f"ConfigurationError was not raised for query: {case['query']}") + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain( self @@ -649,23 +637,7 @@ def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part "expected_error": "Invalid SRV host", }, ] - for case in test_cases: - with patch("dns.resolver.resolve") as mock_resolver: - - def mock_resolve(query, record_type, *args, **kwargs): - mock_srv = MagicMock() - mock_srv.target.to_text.return_value = case["mock_target"] - return [mock_srv] - - mock_resolver.side_effect = mock_resolve - domain = case["query"].split("._tcp.")[1] - connection_string = f"mongodb+srv://{domain}" - try: - parse_uri(connection_string) - except ConfigurationError as e: - self.assertIn(case["expected_error"], str(e)) - else: - self.fail(f"ConfigurationError was not raised for query: {case['query']}") + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) if __name__ == "__main__": From b6920f1110b3602d9525232169050fb39ed7baf1 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 2 Apr 2025 16:57:18 -0400 Subject: [PATCH 14/22] Refactor --- pymongo/asynchronous/srv_resolver.py | 8 ++------ pymongo/synchronous/srv_resolver.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 4885a48bff..ef152b85df 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -90,13 +90,9 @@ def __init__( raise ConfigurationError(_INVALID_HOST_MSG % ("an IP address",)) except ValueError: pass - try: - self.__plist = ( - self.__fqdn.split(".")[1:] - if len(self.__fqdn.split(".")) > 2 - else self.__fqdn.split(".") - ) + split_fqdn = self.__fqdn.split(".") + self.__plist = split_fqdn[1:] if len(split_fqdn) > 2 else split_fqdn except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index 771924a320..88c3dc8365 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -90,13 +90,9 @@ def __init__( raise ConfigurationError(_INVALID_HOST_MSG % ("an IP address",)) except ValueError: pass - try: - self.__plist = ( - self.__fqdn.split(".")[1:] - if len(self.__fqdn.split(".")) > 2 - else self.__fqdn.split(".") - ) + split_fqdn = self.__fqdn.split(".") + self.__plist = split_fqdn[1:] if len(split_fqdn) > 2 else split_fqdn except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) From f72cf8725f3064fd5293a8920c782044a86e9538 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 2 Apr 2025 17:07:23 -0400 Subject: [PATCH 15/22] Refactor --- pymongo/asynchronous/srv_resolver.py | 10 +++++----- pymongo/synchronous/srv_resolver.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index ef152b85df..8dc027a11e 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -129,11 +129,6 @@ async def _get_srv_response_and_hosts( ) -> tuple[resolver.Answer, list[tuple[str, Any]]]: results = await self._resolve_uri(encapsulate_errors) - if self.__fqdn == results[0].target.to_text(): - raise ConfigurationError( - "Invalid SRV host: return address is identical to SRV hostname" - ) - # Construct address tuples nodes = [ (maybe_decode(res.target.to_text(omit_final_dot=True)), res.port) # type: ignore[attr-defined] @@ -141,7 +136,12 @@ async def _get_srv_response_and_hosts( ] # Validate hosts + srv_host = results[0].target.to_text() for node in nodes: + if self.__fqdn == srv_host: + raise ConfigurationError( + "Invalid SRV host: return address is identical to SRV hostname" + ) try: nlist = node[0].lower().split(".")[1:][-self.__slen :] except Exception: diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index 88c3dc8365..da76d1c139 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -129,11 +129,6 @@ def _get_srv_response_and_hosts( ) -> tuple[resolver.Answer, list[tuple[str, Any]]]: results = self._resolve_uri(encapsulate_errors) - if self.__fqdn == results[0].target.to_text(): - raise ConfigurationError( - "Invalid SRV host: return address is identical to SRV hostname" - ) - # Construct address tuples nodes = [ (maybe_decode(res.target.to_text(omit_final_dot=True)), res.port) # type: ignore[attr-defined] @@ -141,7 +136,12 @@ def _get_srv_response_and_hosts( ] # Validate hosts + srv_host = results[0].target.to_text() for node in nodes: + if self.__fqdn == srv_host: + raise ConfigurationError( + "Invalid SRV host: return address is identical to SRV hostname" + ) try: nlist = node[0].lower().split(".")[1:][-self.__slen :] except Exception: From ae5065d9f716b4c1aeffb03cfd15a978b64a2506 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Thu, 3 Apr 2025 13:10:00 -0400 Subject: [PATCH 16/22] Move to test_dns --- test/asynchronous/test_dns.py | 90 +++++++++++++++++++++++++++++++++++ test/test_dns.py | 90 +++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/test/asynchronous/test_dns.py b/test/asynchronous/test_dns.py index 7316b4df9a..a71d89308b 100644 --- a/test/asynchronous/test_dns.py +++ b/test/asynchronous/test_dns.py @@ -30,6 +30,7 @@ unittest, ) from test.utils_shared import async_wait_until +from unittest.mock import MagicMock, patch from pymongo.asynchronous.uri_parser import parse_uri from pymongo.common import validate_read_preference_tags @@ -39,6 +40,26 @@ _IS_SYNC = False +def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): + for case in test_cases: + with patch("dns.resolver.resolve") as mock_resolver: + + def mock_resolve(query, record_type, *args, **kwargs): + mock_srv = MagicMock() + mock_srv.target.to_text.return_value = case["mock_target"] + return [mock_srv] + + mock_resolver.side_effect = mock_resolve + domain = case["query"].split("._tcp.")[1] + connection_string = f"mongodb+srv://{domain}" + try: + parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") + + class TestDNSRepl(AsyncPyMongoTestCase): if _IS_SYNC: TEST_PATH = os.path.join( @@ -201,5 +222,74 @@ async def test_connect_case_insensitive(self): self.assertGreater(len(client.topology_description.server_descriptions()), 1) +class TestInitialDnsSeedlistDiscovery(AsyncPyMongoTestCase): + """ + Initial DNS Seedlist Discovery prose tests + https://github.com/mongodb/specifications/blob/0a7a8b5/source/initial-dns-seedlist-discovery/tests/README.md#prose-tests + """ + + def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): + with patch("dns.resolver.resolve"): + parse_uri("mongodb+srv://localhost/") + parse_uri("mongodb+srv://mongo.local/") + + def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): + test_cases = [ + { + "query": "_mongodb._tcp.localhost", + "mock_target": "localhost.mongodb", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "blogs.evil.com", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongo.local", + "mock_target": "test_1.evil.com", + "expected_error": "Invalid SRV host", + }, + ] + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + + def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): + test_cases = [ + { + "query": "_mongodb._tcp.localhost", + "mock_target": "localhost", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.mongo.local", + "mock_target": "mongo.local", + "expected_error": "Invalid SRV host", + }, + ] + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + + def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain( + self + ): + test_cases = [ + { + "query": "_mongodb._tcp.localhost", + "mock_target": "test_1.cluster_1localhost", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.mongo.local", + "mock_target": "test_1.my_hostmongo.local", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "cluster.testmongodb.com", + "expected_error": "Invalid SRV host", + }, + ] + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + + if __name__ == "__main__": unittest.main() diff --git a/test/test_dns.py b/test/test_dns.py index c478e481f6..b28ebf3042 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -30,6 +30,7 @@ unittest, ) from test.utils_shared import wait_until +from unittest.mock import MagicMock, patch from pymongo.common import validate_read_preference_tags from pymongo.errors import ConfigurationError @@ -39,6 +40,26 @@ _IS_SYNC = True +def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): + for case in test_cases: + with patch("dns.resolver.resolve") as mock_resolver: + + def mock_resolve(query, record_type, *args, **kwargs): + mock_srv = MagicMock() + mock_srv.target.to_text.return_value = case["mock_target"] + return [mock_srv] + + mock_resolver.side_effect = mock_resolve + domain = case["query"].split("._tcp.")[1] + connection_string = f"mongodb+srv://{domain}" + try: + parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") + + class TestDNSRepl(PyMongoTestCase): if _IS_SYNC: TEST_PATH = os.path.join( @@ -199,5 +220,74 @@ def test_connect_case_insensitive(self): self.assertGreater(len(client.topology_description.server_descriptions()), 1) +class TestInitialDnsSeedlistDiscovery(PyMongoTestCase): + """ + Initial DNS Seedlist Discovery prose tests + https://github.com/mongodb/specifications/blob/0a7a8b5/source/initial-dns-seedlist-discovery/tests/README.md#prose-tests + """ + + def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): + with patch("dns.resolver.resolve"): + parse_uri("mongodb+srv://localhost/") + parse_uri("mongodb+srv://mongo.local/") + + def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): + test_cases = [ + { + "query": "_mongodb._tcp.localhost", + "mock_target": "localhost.mongodb", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "blogs.evil.com", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongo.local", + "mock_target": "test_1.evil.com", + "expected_error": "Invalid SRV host", + }, + ] + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + + def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): + test_cases = [ + { + "query": "_mongodb._tcp.localhost", + "mock_target": "localhost", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.mongo.local", + "mock_target": "mongo.local", + "expected_error": "Invalid SRV host", + }, + ] + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + + def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain( + self + ): + test_cases = [ + { + "query": "_mongodb._tcp.localhost", + "mock_target": "test_1.cluster_1localhost", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.mongo.local", + "mock_target": "test_1.my_hostmongo.local", + "expected_error": "Invalid SRV host", + }, + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "cluster.testmongodb.com", + "expected_error": "Invalid SRV host", + }, + ] + run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + + if __name__ == "__main__": unittest.main() From 0e48ce5cd8d4eb0407121edcb4804c8782180f3f Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Thu, 3 Apr 2025 13:11:47 -0400 Subject: [PATCH 17/22] Move to test_dns --- test/test_uri_parser.py | 87 +---------------------------------------- 1 file changed, 1 insertion(+), 86 deletions(-) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 33ee0bc12c..d4d17ac211 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -24,7 +24,7 @@ sys.path[0:0] = [""] from test import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import patch from bson.binary import JAVA_LEGACY from pymongo import ReadPreference @@ -37,26 +37,6 @@ ) -def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): - for case in test_cases: - with patch("dns.resolver.resolve") as mock_resolver: - - def mock_resolve(query, record_type, *args, **kwargs): - mock_srv = MagicMock() - mock_srv.target.to_text.return_value = case["mock_target"] - return [mock_srv] - - mock_resolver.side_effect = mock_resolve - domain = case["query"].split("._tcp.")[1] - connection_string = f"mongodb+srv://{domain}" - try: - parse_uri(connection_string) - except ConfigurationError as e: - self.assertIn(case["expected_error"], str(e)) - else: - self.fail(f"ConfigurationError was not raised for query: {case['query']}") - - class TestURI(unittest.TestCase): def test_validate_userinfo(self): self.assertRaises(InvalidURI, parse_userinfo, "foo@") @@ -574,71 +554,6 @@ def test_port_with_whitespace(self): with self.assertRaisesRegex(ValueError, r"Port contains whitespace character: '\\n'"): parse_uri("mongodb://localhost:27\n017") - # Initial DNS Seedlist Discovery prose tests - # https://github.com/mongodb/specifications/blob/0a7a8b5/source/initial-dns-seedlist-discovery/tests/README.md#prose-tests - - def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): - with patch("dns.resolver.resolve"): - parse_uri("mongodb+srv://localhost/") - parse_uri("mongodb+srv://mongo.local/") - - def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): - test_cases = [ - { - "query": "_mongodb._tcp.localhost", - "mock_target": "localhost.mongodb", - "expected_error": "Invalid SRV host", - }, - { - "query": "_mongodb._tcp.blogs.mongodb.com", - "mock_target": "blogs.evil.com", - "expected_error": "Invalid SRV host", - }, - { - "query": "_mongodb._tcp.blogs.mongo.local", - "mock_target": "test_1.evil.com", - "expected_error": "Invalid SRV host", - }, - ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) - - def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): - test_cases = [ - { - "query": "_mongodb._tcp.localhost", - "mock_target": "localhost", - "expected_error": "Invalid SRV host", - }, - { - "query": "_mongodb._tcp.mongo.local", - "mock_target": "mongo.local", - "expected_error": "Invalid SRV host", - }, - ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) - - def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain( - self - ): - test_cases = [ - { - "query": "_mongodb._tcp.localhost", - "mock_target": "test_1.cluster_1localhost", - "expected_error": "Invalid SRV host", - }, - { - "query": "_mongodb._tcp.mongo.local", - "mock_target": "test_1.my_hostmongo.local", - "expected_error": "Invalid SRV host", - }, - { - "query": "_mongodb._tcp.blogs.mongodb.com", - "mock_target": "cluster.testmongodb.com", - "expected_error": "Invalid SRV host", - }, - ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) - if __name__ == "__main__": unittest.main() From a16b3f6990b4500e6d00b816c4f7f805d171b9bb Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Thu, 3 Apr 2025 15:39:42 -0400 Subject: [PATCH 18/22] Fix async --- test/asynchronous/test_dns.py | 61 +++++++++++++++++------------------ test/test_dns.py | 47 +++++++++++++-------------- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/test/asynchronous/test_dns.py b/test/asynchronous/test_dns.py index a71d89308b..01c8d7b40b 100644 --- a/test/asynchronous/test_dns.py +++ b/test/asynchronous/test_dns.py @@ -40,26 +40,6 @@ _IS_SYNC = False -def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): - for case in test_cases: - with patch("dns.resolver.resolve") as mock_resolver: - - def mock_resolve(query, record_type, *args, **kwargs): - mock_srv = MagicMock() - mock_srv.target.to_text.return_value = case["mock_target"] - return [mock_srv] - - mock_resolver.side_effect = mock_resolve - domain = case["query"].split("._tcp.")[1] - connection_string = f"mongodb+srv://{domain}" - try: - parse_uri(connection_string) - except ConfigurationError as e: - self.assertIn(case["expected_error"], str(e)) - else: - self.fail(f"ConfigurationError was not raised for query: {case['query']}") - - class TestDNSRepl(AsyncPyMongoTestCase): if _IS_SYNC: TEST_PATH = os.path.join( @@ -228,12 +208,31 @@ class TestInitialDnsSeedlistDiscovery(AsyncPyMongoTestCase): https://github.com/mongodb/specifications/blob/0a7a8b5/source/initial-dns-seedlist-discovery/tests/README.md#prose-tests """ - def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): - with patch("dns.resolver.resolve"): - parse_uri("mongodb+srv://localhost/") - parse_uri("mongodb+srv://mongo.local/") - - def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): + async def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): + for case in test_cases: + with patch("dns.asyncresolver.resolve") as mock_resolver: + + async def mock_resolve(query, record_type, *args, **kwargs): + mock_srv = MagicMock() + mock_srv.target.to_text.return_value = case["mock_target"] + return [mock_srv] + + mock_resolver.side_effect = mock_resolve + domain = case["query"].split("._tcp.")[1] + connection_string = f"mongodb+srv://{domain}" + try: + await parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") + + async def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): + with patch("dns.asyncresolver.resolve"): + await parse_uri("mongodb+srv://localhost/") + await parse_uri("mongodb+srv://mongo.local/") + + async def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): test_cases = [ { "query": "_mongodb._tcp.localhost", @@ -251,9 +250,9 @@ def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): "expected_error": "Invalid SRV host", }, ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) - def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): + async def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): test_cases = [ { "query": "_mongodb._tcp.localhost", @@ -266,9 +265,9 @@ def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): "expected_error": "Invalid SRV host", }, ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) - def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain( + async def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain( self ): test_cases = [ @@ -288,7 +287,7 @@ def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part "expected_error": "Invalid SRV host", }, ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) if __name__ == "__main__": diff --git a/test/test_dns.py b/test/test_dns.py index b28ebf3042..b07c0e3227 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -40,26 +40,6 @@ _IS_SYNC = True -def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): - for case in test_cases: - with patch("dns.resolver.resolve") as mock_resolver: - - def mock_resolve(query, record_type, *args, **kwargs): - mock_srv = MagicMock() - mock_srv.target.to_text.return_value = case["mock_target"] - return [mock_srv] - - mock_resolver.side_effect = mock_resolve - domain = case["query"].split("._tcp.")[1] - connection_string = f"mongodb+srv://{domain}" - try: - parse_uri(connection_string) - except ConfigurationError as e: - self.assertIn(case["expected_error"], str(e)) - else: - self.fail(f"ConfigurationError was not raised for query: {case['query']}") - - class TestDNSRepl(PyMongoTestCase): if _IS_SYNC: TEST_PATH = os.path.join( @@ -226,8 +206,27 @@ class TestInitialDnsSeedlistDiscovery(PyMongoTestCase): https://github.com/mongodb/specifications/blob/0a7a8b5/source/initial-dns-seedlist-discovery/tests/README.md#prose-tests """ + def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): + for case in test_cases: + with patch("dns.asyncresolver.resolve") as mock_resolver: + + def mock_resolve(query, record_type, *args, **kwargs): + mock_srv = MagicMock() + mock_srv.target.to_text.return_value = case["mock_target"] + return [mock_srv] + + mock_resolver.side_effect = mock_resolve + domain = case["query"].split("._tcp.")[1] + connection_string = f"mongodb+srv://{domain}" + try: + parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") + def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): - with patch("dns.resolver.resolve"): + with patch("dns.asyncresolver.resolve"): parse_uri("mongodb+srv://localhost/") parse_uri("mongodb+srv://mongo.local/") @@ -249,7 +248,7 @@ def test_2_throw_when_return_address_does_not_end_with_srv_domain(self): "expected_error": "Invalid SRV host", }, ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): test_cases = [ @@ -264,7 +263,7 @@ def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): "expected_error": "Invalid SRV host", }, ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part_of_domain( self @@ -286,7 +285,7 @@ def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part "expected_error": "Invalid SRV host", }, ] - run_initial_dns_seedlist_discovery_prose_tests(self, test_cases) + self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) if __name__ == "__main__": From b1ab0857308263e41af215179c37053fea354288 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Thu, 3 Apr 2025 16:06:34 -0400 Subject: [PATCH 19/22] Fix async --- test/asynchronous/test_dns.py | 2 +- test/test_dns.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/asynchronous/test_dns.py b/test/asynchronous/test_dns.py index 01c8d7b40b..e22c09cbb2 100644 --- a/test/asynchronous/test_dns.py +++ b/test/asynchronous/test_dns.py @@ -210,7 +210,7 @@ class TestInitialDnsSeedlistDiscovery(AsyncPyMongoTestCase): async def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): for case in test_cases: - with patch("dns.asyncresolver.resolve") as mock_resolver: + with patch("pymongo.asynchronous.srv_resolver._resolve") as mock_resolver: async def mock_resolve(query, record_type, *args, **kwargs): mock_srv = MagicMock() diff --git a/test/test_dns.py b/test/test_dns.py index b07c0e3227..11193026ea 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -208,7 +208,7 @@ class TestInitialDnsSeedlistDiscovery(PyMongoTestCase): def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): for case in test_cases: - with patch("dns.asyncresolver.resolve") as mock_resolver: + with patch("pymongo.srv_resolver._resolve") as mock_resolver: def mock_resolve(query, record_type, *args, **kwargs): mock_srv = MagicMock() From 4e3d524682650990e686fede684badc5dc601d3f Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Thu, 3 Apr 2025 16:08:09 -0400 Subject: [PATCH 20/22] Revert "Fix async" This reverts commit b1ab0857308263e41af215179c37053fea354288. --- test/asynchronous/test_dns.py | 2 +- test/test_dns.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/asynchronous/test_dns.py b/test/asynchronous/test_dns.py index e22c09cbb2..01c8d7b40b 100644 --- a/test/asynchronous/test_dns.py +++ b/test/asynchronous/test_dns.py @@ -210,7 +210,7 @@ class TestInitialDnsSeedlistDiscovery(AsyncPyMongoTestCase): async def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): for case in test_cases: - with patch("pymongo.asynchronous.srv_resolver._resolve") as mock_resolver: + with patch("dns.asyncresolver.resolve") as mock_resolver: async def mock_resolve(query, record_type, *args, **kwargs): mock_srv = MagicMock() diff --git a/test/test_dns.py b/test/test_dns.py index 11193026ea..b07c0e3227 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -208,7 +208,7 @@ class TestInitialDnsSeedlistDiscovery(PyMongoTestCase): def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): for case in test_cases: - with patch("pymongo.srv_resolver._resolve") as mock_resolver: + with patch("dns.asyncresolver.resolve") as mock_resolver: def mock_resolve(query, record_type, *args, **kwargs): mock_srv = MagicMock() From 531e9c929e986d5c6bb92d0ec1c5e09562a3d5dd Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Thu, 3 Apr 2025 16:13:19 -0400 Subject: [PATCH 21/22] Add dns.resolver.resolve to synchro --- test/test_dns.py | 4 ++-- tools/synchro.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_dns.py b/test/test_dns.py index b07c0e3227..9360f3f289 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -208,7 +208,7 @@ class TestInitialDnsSeedlistDiscovery(PyMongoTestCase): def run_initial_dns_seedlist_discovery_prose_tests(self, test_cases): for case in test_cases: - with patch("dns.asyncresolver.resolve") as mock_resolver: + with patch("dns.resolver.resolve") as mock_resolver: def mock_resolve(query, record_type, *args, **kwargs): mock_srv = MagicMock() @@ -226,7 +226,7 @@ def mock_resolve(query, record_type, *args, **kwargs): self.fail(f"ConfigurationError was not raised for query: {case['query']}") def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): - with patch("dns.asyncresolver.resolve"): + with patch("dns.resolver.resolve"): parse_uri("mongodb+srv://localhost/") parse_uri("mongodb+srv://mongo.local/") diff --git a/tools/synchro.py b/tools/synchro.py index f451d09a26..37bf9bc740 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -133,6 +133,7 @@ "async_joinall": "joinall", "_async_create_connection": "_create_connection", "pymongo.asynchronous.srv_resolver._SrvResolver.get_hosts": "pymongo.synchronous.srv_resolver._SrvResolver.get_hosts", + "dns.asyncresolver.resolve": "dns.resolver.resolve", } docstring_replacements: dict[tuple[str, str], str] = { From dacdbf7d11c92181cdd63dd81d48ba1095808d7e Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Thu, 3 Apr 2025 19:01:02 -0400 Subject: [PATCH 22/22] Validate each return host in nodes --- pymongo/asynchronous/srv_resolver.py | 3 +-- pymongo/synchronous/srv_resolver.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 8dc027a11e..f7c67af3e1 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -136,9 +136,8 @@ async def _get_srv_response_and_hosts( ] # Validate hosts - srv_host = results[0].target.to_text() for node in nodes: - if self.__fqdn == srv_host: + if self.__fqdn == node[0].lower(): raise ConfigurationError( "Invalid SRV host: return address is identical to SRV hostname" ) diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index da76d1c139..cf7b0842ab 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -136,9 +136,8 @@ def _get_srv_response_and_hosts( ] # Validate hosts - srv_host = results[0].target.to_text() for node in nodes: - if self.__fqdn == srv_host: + if self.__fqdn == node[0].lower(): raise ConfigurationError( "Invalid SRV host: return address is identical to SRV hostname" )