@@ -12,14 +12,18 @@ def test_simple_uri(self):
1212 settings_dict = parse_uri ("mongodb://cluster0.example.mongodb.net/myDatabase" )
1313 self .assertEqual (settings_dict ["ENGINE" ], "django_mongodb_backend" )
1414 self .assertEqual (settings_dict ["NAME" ], "myDatabase" )
15- self .assertEqual (settings_dict ["HOST" ], "cluster0.example.mongodb.net" )
16- self .assertEqual (settings_dict ["OPTIONS" ], {"authSource" : "myDatabase" })
15+ # Default authSource derived from URI path db is appended to HOST
16+ self .assertEqual (
17+ settings_dict ["HOST" ], "cluster0.example.mongodb.net?authSource=myDatabase"
18+ )
19+ self .assertEqual (settings_dict ["OPTIONS" ], {})
1720
1821 def test_db_name (self ):
1922 settings_dict = parse_uri ("mongodb://cluster0.example.mongodb.net/" , db_name = "myDatabase" )
2023 self .assertEqual (settings_dict ["ENGINE" ], "django_mongodb_backend" )
2124 self .assertEqual (settings_dict ["NAME" ], "myDatabase" )
2225 self .assertEqual (settings_dict ["HOST" ], "cluster0.example.mongodb.net" )
26+ # No default authSource injected when the URI has no database path
2327 self .assertEqual (settings_dict ["OPTIONS" ], {})
2428
2529 def test_db_name_overrides_default_auth_db (self ):
@@ -28,8 +32,11 @@ def test_db_name_overrides_default_auth_db(self):
2832 )
2933 self .assertEqual (settings_dict ["ENGINE" ], "django_mongodb_backend" )
3034 self .assertEqual (settings_dict ["NAME" ], "myDatabase" )
31- self .assertEqual (settings_dict ["HOST" ], "cluster0.example.mongodb.net" )
32- self .assertEqual (settings_dict ["OPTIONS" ], {"authSource" : "default_auth_db" })
35+ # authSource defaults to the database from the URI, not db_name
36+ self .assertEqual (
37+ settings_dict ["HOST" ], "cluster0.example.mongodb.net?authSource=default_auth_db"
38+ )
39+ self .assertEqual (settings_dict ["OPTIONS" ], {})
3340
3441 def test_no_database (self ):
3542 msg = "You must provide the db_name parameter."
@@ -43,55 +50,71 @@ def test_srv_uri_with_options(self):
4350 with patch ("dns.resolver.resolve" ):
4451 settings_dict = parse_uri (uri )
4552 self .assertEqual (settings_dict ["NAME" ], "my_database" )
46- self .assertEqual (settings_dict ["HOST" ], "mongodb+srv://cluster0.example.mongodb.net" )
53+ # HOST includes scheme + fqdn only (no path), with query
54+ # preserved and default authSource appended
55+ self .assertTrue (
56+ settings_dict ["HOST" ].startswith ("mongodb+srv://cluster0.example.mongodb.net?" )
57+ )
58+ self .assertIn ("retryWrites=true" , settings_dict ["HOST" ])
59+ self .assertIn ("w=majority" , settings_dict ["HOST" ])
60+ self .assertIn ("authSource=my_database" , settings_dict ["HOST" ])
4761 self .assertEqual (settings_dict ["USER" ], "my_user" )
4862 self .assertEqual (settings_dict ["PASSWORD" ], "my_password" )
4963 self .assertIsNone (settings_dict ["PORT" ])
50- self .assertEqual (
51- settings_dict ["OPTIONS" ],
52- {"authSource" : "my_database" , "retryWrites" : True , "w" : "majority" , "tls" : True },
53- )
64+ # No options copied into OPTIONS; they live in HOST query
65+ self .assertEqual (settings_dict ["OPTIONS" ], {})
5466
5567 def test_localhost (self ):
5668 settings_dict = parse_uri ("mongodb://localhost/db" )
57- self .assertEqual (settings_dict ["HOST" ], "localhost" )
69+ # Default authSource appended to HOST
70+ self .assertEqual (settings_dict ["HOST" ], "localhost?authSource=db" )
5871 self .assertEqual (settings_dict ["PORT" ], 27017 )
5972
6073 def test_localhost_with_port (self ):
6174 settings_dict = parse_uri ("mongodb://localhost:27018/db" )
62- self .assertEqual (settings_dict ["HOST" ], "localhost" )
75+ # HOST omits the path and port, keeps only host + query
76+ self .assertEqual (settings_dict ["HOST" ], "localhost?authSource=db" )
6377 self .assertEqual (settings_dict ["PORT" ], 27018 )
6478
6579 def test_hosts_with_ports (self ):
6680 settings_dict = parse_uri ("mongodb://localhost:27017,localhost:27018/db" )
67- self .assertEqual (settings_dict ["HOST" ], "localhost:27017,localhost:27018" )
81+ # For multi-host, PORT is None and HOST carries the full host list plus query
82+ self .assertEqual (settings_dict ["HOST" ], "localhost:27017,localhost:27018?authSource=db" )
6883 self .assertEqual (settings_dict ["PORT" ], None )
6984
7085 def test_hosts_without_ports (self ):
7186 settings_dict = parse_uri ("mongodb://host1.net,host2.net/db" )
72- self .assertEqual (settings_dict ["HOST" ], "host1.net:27017,host2.net:27017" )
87+ # Default ports are added to each host in HOST, plus the query
88+ self .assertEqual (settings_dict ["HOST" ], "host1.net:27017,host2.net:27017?authSource=db" )
7389 self .assertEqual (settings_dict ["PORT" ], None )
7490
7591 def test_auth_source_in_query_string (self ):
7692 settings_dict = parse_uri ("mongodb://localhost/?authSource=auth" , db_name = "db" )
7793 self .assertEqual (settings_dict ["NAME" ], "db" )
78- self .assertEqual (settings_dict ["OPTIONS" ], {"authSource" : "auth" })
94+ # Keep original query intact in HOST; do not duplicate into OPTIONS
95+ self .assertEqual (settings_dict ["HOST" ], "localhost?authSource=auth" )
96+ self .assertEqual (settings_dict ["OPTIONS" ], {})
7997
8098 def test_auth_source_in_query_string_overrides_defaultauthdb (self ):
8199 settings_dict = parse_uri ("mongodb://localhost/db?authSource=auth" )
82100 self .assertEqual (settings_dict ["NAME" ], "db" )
83- self .assertEqual (settings_dict ["OPTIONS" ], {"authSource" : "auth" })
101+ # Query-provided authSource overrides default; kept in HOST only
102+ self .assertEqual (settings_dict ["HOST" ], "localhost?authSource=auth" )
103+ self .assertEqual (settings_dict ["OPTIONS" ], {})
84104
85105 def test_options_kwarg (self ):
86106 options = {"authSource" : "auth" , "retryWrites" : True }
87107 settings_dict = parse_uri (
88108 "mongodb://cluster0.example.mongodb.net/myDatabase?retryWrites=false&retryReads=true" ,
89109 options = options ,
90110 )
91- self .assertEqual (
92- settings_dict ["OPTIONS" ],
93- {"authSource" : "auth" , "retryWrites" : True , "retryReads" : True },
94- )
111+ # options kwarg overrides same-key query params; query-only keys are kept.
112+ # All options live in HOST's query string; OPTIONS is empty.
113+ self .assertTrue (settings_dict ["HOST" ].startswith ("cluster0.example.mongodb.net?" ))
114+ self .assertIn ("authSource=auth" , settings_dict ["HOST" ])
115+ self .assertIn ("retryWrites=true" , settings_dict ["HOST" ]) # overridden
116+ self .assertIn ("retryReads=true" , settings_dict ["HOST" ]) # preserved
117+ self .assertEqual (settings_dict ["OPTIONS" ], {})
95118
96119 def test_test_kwarg (self ):
97120 settings_dict = parse_uri ("mongodb://localhost/db" , test = {"NAME" : "test_db" })
@@ -105,3 +128,51 @@ def test_invalid_credentials(self):
105128 def test_no_scheme (self ):
106129 with self .assertRaisesMessage (pymongo .errors .InvalidURI , "Invalid URI scheme" ):
107130 parse_uri ("cluster0.example.mongodb.net" )
131+
132+ def test_read_preference_tags_in_host_query_allows_mongoclient_construction (self ):
133+ """
134+ Ensure readPreferenceTags preserved in the HOST query string can be parsed by
135+ MongoClient without raising validation errors, and result in correct tag sets.
136+ This verifies we no longer rely on pymongo's normalized options dict for tags.
137+ """
138+ uri = (
139+ "mongodb://localhost/"
140+ "?readPreference=secondary"
141+ "&readPreferenceTags=dc:ny,other:sf"
142+ "&readPreferenceTags=dc:2,other:1"
143+ )
144+
145+ # Baseline: demonstrate why relying on parsed options can be problematic.
146+ parsed = pymongo .uri_parser .parse_uri (uri )
147+ # Some PyMongo versions normalize this into a dict (invalid as a kwarg), others into a list.
148+ # If it's a dict, passing it as a kwarg will raise a ValueError as shown in the issue.
149+ # We only assert no crash in our new path below; this is informational.
150+ if isinstance (parsed ["options" ].get ("readPreferenceTags" ), dict ):
151+ with self .assertRaises (ValueError ):
152+ pymongo .MongoClient (readPreferenceTags = parsed ["options" ]["readPreferenceTags" ])
153+
154+ # New behavior: keep the raw query on HOST, not in OPTIONS.
155+ settings_dict = parse_uri (uri , db_name = "db" )
156+ host_with_query = settings_dict ["HOST" ]
157+ # Compose a full URI for MongoClient (non-SRV -> prepend scheme
158+ # and ensure "/?" before query)
159+ if host_with_query .startswith ("mongodb+srv://" ):
160+ full_uri = host_with_query # SRV already includes scheme
161+ else :
162+ if "?" in host_with_query :
163+ base , q = host_with_query .split ("?" , 1 )
164+ full_uri = f"mongodb://{ base } /?{ q } "
165+ else :
166+ full_uri = f"mongodb://{ host_with_query } /"
167+
168+ # Constructing MongoClient should not raise, and should reflect the read preference + tags.
169+ client = pymongo .MongoClient (full_uri , serverSelectionTimeoutMS = 1 )
170+ try :
171+ doc = client .read_preference .document
172+ self .assertEqual (doc .get ("mode" ), "secondary" )
173+ self .assertEqual (
174+ doc .get ("tags" ),
175+ [{"dc" : "ny" , "other" : "sf" }, {"dc" : "2" , "other" : "1" }],
176+ )
177+ finally :
178+ client .close ()
0 commit comments