@@ -12,14 +12,18 @@ def test_simple_uri(self):
12
12
settings_dict = parse_uri ("mongodb://cluster0.example.mongodb.net/myDatabase" )
13
13
self .assertEqual (settings_dict ["ENGINE" ], "django_mongodb_backend" )
14
14
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" ], {})
17
20
18
21
def test_db_name (self ):
19
22
settings_dict = parse_uri ("mongodb://cluster0.example.mongodb.net/" , db_name = "myDatabase" )
20
23
self .assertEqual (settings_dict ["ENGINE" ], "django_mongodb_backend" )
21
24
self .assertEqual (settings_dict ["NAME" ], "myDatabase" )
22
25
self .assertEqual (settings_dict ["HOST" ], "cluster0.example.mongodb.net" )
26
+ # No default authSource injected when the URI has no database path
23
27
self .assertEqual (settings_dict ["OPTIONS" ], {})
24
28
25
29
def test_db_name_overrides_default_auth_db (self ):
@@ -28,8 +32,11 @@ def test_db_name_overrides_default_auth_db(self):
28
32
)
29
33
self .assertEqual (settings_dict ["ENGINE" ], "django_mongodb_backend" )
30
34
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" ], {})
33
40
34
41
def test_no_database (self ):
35
42
msg = "You must provide the db_name parameter."
@@ -43,55 +50,71 @@ def test_srv_uri_with_options(self):
43
50
with patch ("dns.resolver.resolve" ):
44
51
settings_dict = parse_uri (uri )
45
52
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" ])
47
61
self .assertEqual (settings_dict ["USER" ], "my_user" )
48
62
self .assertEqual (settings_dict ["PASSWORD" ], "my_password" )
49
63
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" ], {})
54
66
55
67
def test_localhost (self ):
56
68
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" )
58
71
self .assertEqual (settings_dict ["PORT" ], 27017 )
59
72
60
73
def test_localhost_with_port (self ):
61
74
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" )
63
77
self .assertEqual (settings_dict ["PORT" ], 27018 )
64
78
65
79
def test_hosts_with_ports (self ):
66
80
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" )
68
83
self .assertEqual (settings_dict ["PORT" ], None )
69
84
70
85
def test_hosts_without_ports (self ):
71
86
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" )
73
89
self .assertEqual (settings_dict ["PORT" ], None )
74
90
75
91
def test_auth_source_in_query_string (self ):
76
92
settings_dict = parse_uri ("mongodb://localhost/?authSource=auth" , db_name = "db" )
77
93
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" ], {})
79
97
80
98
def test_auth_source_in_query_string_overrides_defaultauthdb (self ):
81
99
settings_dict = parse_uri ("mongodb://localhost/db?authSource=auth" )
82
100
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" ], {})
84
104
85
105
def test_options_kwarg (self ):
86
106
options = {"authSource" : "auth" , "retryWrites" : True }
87
107
settings_dict = parse_uri (
88
108
"mongodb://cluster0.example.mongodb.net/myDatabase?retryWrites=false&retryReads=true" ,
89
109
options = options ,
90
110
)
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" ], {})
95
118
96
119
def test_test_kwarg (self ):
97
120
settings_dict = parse_uri ("mongodb://localhost/db" , test = {"NAME" : "test_db" })
@@ -105,3 +128,51 @@ def test_invalid_credentials(self):
105
128
def test_no_scheme (self ):
106
129
with self .assertRaisesMessage (pymongo .errors .InvalidURI , "Invalid URI scheme" ):
107
130
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