From 8dbf41a5ff9c4614ea516708ca921a2673c40cd2 Mon Sep 17 00:00:00 2001 From: Prashant Mital Date: Thu, 25 Apr 2019 16:57:27 -0700 Subject: [PATCH] PYTHON-1827 Follow-on work for unifying URI options --- pymongo/common.py | 98 +++++++++++++- pymongo/mongo_client.py | 28 ++-- pymongo/uri_parser.py | 207 ++++++++++++++---------------- test/test_client.py | 35 +++++ test/test_uri_parser.py | 70 +++------- test/test_uri_spec.py | 2 +- test/uri_options/tls-options.json | 140 +++++++++++++++++++- 7 files changed, 392 insertions(+), 188 deletions(-) diff --git a/pymongo/common.py b/pymongo/common.py index a74e9bdea..7f4334264 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -700,17 +700,25 @@ def get_validated_options(options, warn=True): Returns a copy of options with invalid entries removed. :Parameters: - - `opts`: A dict of MongoDB URI options. + - `opts`: A dict containing MongoDB URI options. - `warn` (optional): If ``True`` then warnings will be logged and invalid options will be ignored. Otherwise, invalid options will cause errors. """ - validated_options = {} + if isinstance(options, _CaseInsensitiveDictionary): + validated_options = _CaseInsensitiveDictionary() + get_normed_key = lambda x: x + get_setter_key = lambda x: options.cased_key(x) + else: + validated_options = {} + get_normed_key = lambda x: x.lower() + get_setter_key = lambda x: x + for opt, value in iteritems(options): - lower = opt.lower() + normed_key = get_normed_key(opt) try: validator = URI_OPTIONS_VALIDATOR_MAP.get( - lower, raise_config_error) + normed_key, raise_config_error) value = validator(opt, value) except (ValueError, TypeError, ConfigurationError) as exc: if warn: @@ -718,7 +726,7 @@ def get_validated_options(options, warn=True): else: raise else: - validated_options[lower] = value + validated_options[get_setter_key(normed_key)] = value return validated_options @@ -814,3 +822,83 @@ class BaseObject(object): .. versionadded:: 3.2 """ return self.__read_concern + + +class _CaseInsensitiveDictionary(abc.MutableMapping): + def __init__(self, *args, **kwargs): + self.__casedkeys = {} + self.__data = {} + self.update(dict(*args, **kwargs)) + + def __contains__(self, key): + return key.lower() in self.__data + + def __len__(self): + return len(self.__data) + + def __iter__(self): + return (key for key in self.__casedkeys) + + def __repr__(self): + return str({self.__casedkeys[k]: self.__data[k] for k in self}) + + def __setitem__(self, key, value): + lc_key = key.lower() + self.__casedkeys[lc_key] = key + self.__data[lc_key] = value + + def __getitem__(self, key): + return self.__data[key.lower()] + + def __delitem__(self, key): + lc_key = key.lower() + del self.__casedkeys[lc_key] + del self.__data[lc_key] + + def __eq__(self, other): + if not isinstance(other, abc.Mapping): + return NotImplemented + if len(self) != len(other): + return False + for key in other: + if self[key] != other[key]: + return False + + return True + + def get(self, key, default=None): + return self.__data.get(key.lower(), default) + + def pop(self, key, *args, **kwargs): + lc_key = key.lower() + self.__casedkeys.pop(lc_key, None) + return self.__data.pop(lc_key, *args, **kwargs) + + def popitem(self): + lc_key, cased_key = self.__casedkeys.popitem() + value = self.__data.pop(lc_key) + return cased_key, value + + def clear(self): + self.__casedkeys.clear() + self.__data.clear() + + def setdefault(self, key, default=None): + lc_key = key.lower() + if key in self: + return self.__data[lc_key] + else: + self.__casedkeys[lc_key] = key + self.__data[lc_key] = default + return default + + def update(self, other): + if isinstance(other, _CaseInsensitiveDictionary): + for key in other: + self[other.cased_key(key)] = other[key] + else: + for key in other: + self[key] = other[key] + + def cased_key(self, key): + return self.__casedkeys[key.lower()] \ No newline at end of file diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index 106a800a7..6521ac97e 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -39,7 +39,7 @@ import weakref from collections import defaultdict -from bson.codec_options import DEFAULT_CODEC_OPTIONS, TypeRegistry +from bson.codec_options import DEFAULT_CODEC_OPTIONS from bson.py3compat import (integer_types, string_type) from bson.son import SON @@ -71,8 +71,8 @@ from pymongo.server_type import SERVER_TYPE from pymongo.topology import Topology from pymongo.topology_description import TOPOLOGY_TYPE from pymongo.settings import TopologySettings -from pymongo.uri_parser import (_CaseInsensitiveDictionary, - _handle_option_deprecations, +from pymongo.uri_parser import (_handle_option_deprecations, + _handle_security_options, _normalize_options) from pymongo.write_concern import DEFAULT_WRITE_CONCERN @@ -588,7 +588,7 @@ class MongoClient(common.BaseObject): for entity in host: if "://" in entity: res = uri_parser.parse_uri( - entity, port, validate=True, warn=True) + entity, port, validate=True, warn=True, normalize=False) seeds.update(res["nodelist"]) username = res["username"] or username password = res["password"] or password @@ -605,7 +605,7 @@ class MongoClient(common.BaseObject): monitor_class = kwargs.pop('_monitor_class', None) condition_class = kwargs.pop('_condition_class', None) - keyword_opts = kwargs + keyword_opts = common._CaseInsensitiveDictionary(kwargs) keyword_opts['document_class'] = document_class if type_registry is not None: keyword_opts['type_registry'] = type_registry @@ -616,15 +616,19 @@ class MongoClient(common.BaseObject): keyword_opts['tz_aware'] = tz_aware keyword_opts['connect'] = connect - # Validate kwargs options. - keyword_opts = _CaseInsensitiveDictionary( - dict(common.validate(k, v) for k, v in keyword_opts.items())) - # Handle deprecated options in kwarg list. + # Handle deprecated options in kwarg options. keyword_opts = _handle_option_deprecations(keyword_opts) - # Change kwarg option names to those used internally. - keyword_opts = _normalize_options(keyword_opts) - # Augment URI options with kwarg options, overriding the former. + # Validate kwarg options. + keyword_opts = common._CaseInsensitiveDictionary( + dict(common.validate(k, v) for k, v in keyword_opts.items())) + + # Override connection string options with kwarg options. opts.update(keyword_opts) + # Handle security-option conflicts in combined options. + opts = _handle_security_options(opts) + # Normalize combined options. + opts = _normalize_options(opts) + # Username and password passed as kwargs override user info in URI. username = opts.get("username", username) password = opts.get("password", password) diff --git a/pymongo/uri_parser.py b/pymongo/uri_parser.py index 447936b11..e44a86023 100644 --- a/pymongo/uri_parser.py +++ b/pymongo/uri_parser.py @@ -23,7 +23,7 @@ try: except ImportError: _HAVE_DNSPYTHON = False -from bson.py3compat import abc, iteritems, string_type, PY3 +from bson.py3compat import string_type, PY3 if PY3: from urllib.parse import unquote_plus @@ -31,7 +31,8 @@ else: from urllib import unquote_plus from pymongo.common import ( - get_validated_options, URI_OPTIONS_DEPRECATION_MAP, INTERNAL_URI_OPTION_NAME_MAP) + get_validated_options, INTERNAL_URI_OPTION_NAME_MAP, + URI_OPTIONS_DEPRECATION_MAP, _CaseInsensitiveDictionary) from pymongo.errors import ConfigurationError, InvalidURI @@ -42,80 +43,6 @@ SRV_SCHEME_LEN = len(SRV_SCHEME) DEFAULT_PORT = 27017 -class _CaseInsensitiveDictionary(abc.MutableMapping): - def __init__(self, *args, **kwargs): - self.__casedkeys = {} - self.__data = {} - self.update(dict(*args, **kwargs)) - - def __contains__(self, key): - return key.lower() in self.__data - - def __len__(self): - return len(self.__data) - - def __iter__(self): - return (self.__casedkeys[key] for key in self.__casedkeys) - - def __repr__(self): - return str(self.__data) - - def __setitem__(self, key, value): - lc_key = key.lower() - self.__casedkeys[lc_key] = key - self.__data[lc_key] = value - - def __getitem__(self, key): - return self.__data[key.lower()] - - def __delitem__(self, key): - lc_key = key.lower() - del self.__casedkeys[lc_key] - del self.__data[lc_key] - - def get(self, key, default=None): - lc_key = key.lower() - if lc_key in self: - return self.__data[lc_key] - return default - - def pop(self, key, *args, **kwargs): - lc_key = key.lower() - self.__casedkeys.pop(lc_key, None) - return self.__data.pop(lc_key, *args, **kwargs) - - def popitem(self): - lc_key, cased_key = self.__casedkeys.popitem() - value = self.__data.pop(lc_key) - return cased_key, value - - def clear(self): - self.__casedkeys.clear() - self.__data.clear() - - def setdefault(self, key, default=None): - lc_key = key.lower() - if key in self: - return self.__data[lc_key] - else: - self.__casedkeys[lc_key] = key - self.__data[lc_key] = default - return default - - def update(self, other): - for key in other: - self[key] = other[key] - - def cased_key(self, key): - return self.__casedkeys[key.lower()] - - def as_dict(self): - lc_data = {} - for lc_key in self.__data: - lc_data[lc_key] = self.__data[lc_key] - return lc_data - - def parse_userinfo(userinfo): """Validates the format of user information in a MongoDB URI. Reserved characters like ':', '/', '+' and '@' must be escaped @@ -207,11 +134,15 @@ def parse_host(entity, default_port=DEFAULT_PORT): _IMPLICIT_TLSINSECURE_OPTS = {"tlsallowinvalidcertificates", "tlsallowinvalidhostnames"} +_TLSINSECURE_EXCLUDE_OPTS = (_IMPLICIT_TLSINSECURE_OPTS | + {INTERNAL_URI_OPTION_NAME_MAP[k] for k in + _IMPLICIT_TLSINSECURE_OPTS}) + def _parse_options(opts, delim): """Helper method for split_options which creates the options dict. Also handles the creation of a list for the URI tag_sets/ - readpreferencetags portion and the use of the tlsInsecure option.""" + readpreferencetags portion, and the use of a unicode options string.""" options = _CaseInsensitiveDictionary() for uriopt in opts.split(delim): key, value = uriopt.split("=") @@ -222,14 +153,38 @@ def _parse_options(opts, delim): warnings.warn("Duplicate URI option '%s'." % (key,)) options[key] = unquote_plus(value) - if 'tlsInsecure' in options: - for implicit_option in _IMPLICIT_TLSINSECURE_OPTS: - if implicit_option in options: - warn_msg = "URI option '%s' overrides value implied by '%s'." - warnings.warn(warn_msg % (options.cased_key(implicit_option), - options.cased_key('tlsInsecure'))) - continue - options[implicit_option] = options['tlsInsecure'] + return options + + +def _handle_security_options(options): + """Raise appropriate errors when conflicting TLS options are present in + the options dictionary. + + :Parameters: + - `options`: Instance of _CaseInsensitiveDictionary containing + MongoDB URI options. + """ + tlsinsecure = options.get('tlsinsecure') + if tlsinsecure is not None: + for opt in _TLSINSECURE_EXCLUDE_OPTS: + if opt in options: + err_msg = ("URI options %s and %s cannot be specified " + "simultaneously.") + raise InvalidURI(err_msg % ( + options.cased_key('tlsinsecure'), options.cased_key(opt))) + + if 'ssl' in options and 'tls' in options: + def truth_value(val): + if val in ('true', 'false'): + return val == 'true' + if isinstance(val, bool): + return val + return val + if truth_value(options.get('ssl')) != truth_value(options.get('tls')): + err_msg = ("Can not specify conflicting values for URI options %s " + "and %s.") + raise InvalidURI(err_msg % ( + options.cased_key('ssl'), options.cased_key('tls'))) return options @@ -237,31 +192,49 @@ def _parse_options(opts, delim): def _handle_option_deprecations(options): """Issue appropriate warnings when deprecated options are present in the options dictionary. Removes deprecated option key, value pairs if the - options dictionary is found to also have the renamed option.""" - undeprecated_options = _CaseInsensitiveDictionary() - for key, value in iteritems(options): - optname = str(key).lower() + options dictionary is found to also have the renamed option. + + :Parameters: + - `options`: Instance of _CaseInsensitiveDictionary containing + MongoDB URI options. + """ + for optname in list(options): if optname in URI_OPTIONS_DEPRECATION_MAP: - renamed_key = URI_OPTIONS_DEPRECATION_MAP[optname] - if renamed_key.lower() in options: - warnings.warn("Deprecated option '%s' ignored in favor of " - "'%s'." % (str(key), renamed_key)) + newoptname = URI_OPTIONS_DEPRECATION_MAP[optname] + if newoptname in options: + warn_msg = "Deprecated option '%s' ignored in favor of '%s'." + warnings.warn(warn_msg % (options.cased_key(optname), + options.cased_key(newoptname))) + options.pop(optname) continue - warnings.warn("Option '%s' is deprecated, use '%s' instead." % ( - str(key), renamed_key)) - undeprecated_options[str(key)] = value - return undeprecated_options + warn_msg = "Option '%s' is deprecated, use '%s' instead." + warnings.warn(warn_msg % (options.cased_key(optname), + newoptname)) + + return options def _normalize_options(options): - """Renames keys in the options dictionary to their internally-used - names.""" - normalized_options = {} - for key, value in iteritems(options): - optname = str(key).lower() - intname = INTERNAL_URI_OPTION_NAME_MAP.get(optname, key) - normalized_options[intname] = options[key] - return normalized_options + """Normalizes option names in the options dictionary by converting them to + their internally-used names. Also handles use of the tlsInsecure option. + + :Parameters: + - `options`: Instance of _CaseInsensitiveDictionary containing + MongoDB URI options. + """ + tlsinsecure = options.get('tlsinsecure') + if tlsinsecure is not None: + for opt in _IMPLICIT_TLSINSECURE_OPTS: + intname = INTERNAL_URI_OPTION_NAME_MAP.get(opt, None) + # Internal options are logical inverse of public options. + options[intname] = not tlsinsecure + + for optname in list(options): + intname = INTERNAL_URI_OPTION_NAME_MAP.get(optname, None) + if intname is not None: + options[intname] = options.pop(optname) + + return options def validate_options(opts, warn=False): @@ -309,6 +282,8 @@ def split_options(opts, validate=True, warn=False, normalize=True): except ValueError: raise InvalidURI("MongoDB URI options are key=value pairs.") + options = _handle_security_options(options) + options = _handle_option_deprecations(options) if validate: @@ -390,7 +365,8 @@ def _get_dns_txt_options(hostname): b'&'.join([b''.join(res.strings) for res in results])).decode('utf-8') -def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False): +def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False, + normalize=True): """Parse and validate a MongoDB URI. Returns a dict of the form:: @@ -411,15 +387,20 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False): - `uri`: The MongoDB URI to parse. - `default_port`: The port number to use when one wasn't specified for a host in the URI. - - `validate`: If ``True`` (the default), validate and normalize all - options. + - `validate` (optional): If ``True`` (the default), validate and + normalize all options. Default: ``True``. - `warn` (optional): When validating, if ``True`` then will warn the user then ignore any invalid options or values. If ``False``, validation will error when options are unsupported or values are - invalid. + invalid. Default: ``False``. + - `normalize` (optional): If ``True``, convert names of URI options + to their internally-used names. Default: ``True``. + + .. versionchanged:: 3.9 + Added the ``normalize`` parameter. .. versionchanged:: 3.6 - Added support for mongodb+srv:// URIs + Added support for mongodb+srv:// URIs. .. versionchanged:: 3.5 Return the original value of the ``readPreference`` MongoDB URI option @@ -448,7 +429,7 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False): passwd = None dbase = None collection = None - options = {} + options = _CaseInsensitiveDictionary() host_part, _, path_part = scheme_free.partition('/') if not host_part: @@ -500,7 +481,7 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False): dns_options = _get_dns_txt_options(fqdn) if dns_options: - options = split_options(dns_options, validate, warn) + options = split_options(dns_options, validate, warn, normalize) if set(options) - _ALLOWED_TXT_OPTS: raise ConfigurationError( "Only authSource and replicaSet are supported from DNS") @@ -520,7 +501,7 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False): raise InvalidURI('Bad database name "%s"' % dbase) if opts: - options.update(split_options(opts, validate, warn)) + options.update(split_options(opts, validate, warn, normalize)) if dbase is not None: dbase = unquote_plus(dbase) diff --git a/test/test_client.py b/test/test_client.py index e8c41583a..5bb57464f 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -344,6 +344,41 @@ class ClientUnitTest(unittest.TestCase): c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) + def test_uri_option_precedence(self): + # Ensure kwarg options override connection string options. + uri = ("mongodb://localhost/?ssl=true&replicaSet=name" + "&readPreference=primary") + c = MongoClient(uri, ssl=False, replicaSet="newname", + readPreference="secondaryPreferred") + clopts = c._MongoClient__options + opts = clopts._options + + self.assertEqual(opts['ssl'], False) + self.assertEqual(clopts.replica_set_name, "newname") + self.assertEqual( + clopts.read_preference, ReadPreference.SECONDARY_PREFERRED) + + def test_uri_security_options(self): + # Ensure that we don't silently override security-related options. + with self.assertRaises(InvalidURI): + MongoClient('mongodb://localhost/?ssl=true', tls=False, + connect=False) + + # Matching SSL and TLS options should not cause errors. + c = MongoClient('mongodb://localhost/?ssl=false', tls=False, + connect=False) + self.assertEqual(c._MongoClient__options._options['ssl'], False) + + # Conflicting tlsInsecure options should raise an error. + with self.assertRaises(InvalidURI): + MongoClient('mongodb://localhost/?tlsInsecure=true', + connect=False, tlsAllowInvalidHostnames=True) + + # Conflicting legacy tlsInsecure options should also raise an error. + with self.assertRaises(InvalidURI): + MongoClient('mongodb://localhost/?tlsInsecure=true', + connect=False, ssl_cert_reqs=True) + class TestClient(IntegrationTest): diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 5271f2cd7..870953595 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -24,13 +24,12 @@ from pymongo.uri_parser import (parse_userinfo, split_hosts, split_options, parse_uri) -from pymongo.common import get_validated_options from pymongo.errors import ConfigurationError, InvalidURI from pymongo.ssl_support import ssl from pymongo import ReadPreference from bson.binary import JAVA_LEGACY from bson.py3compat import string_type, _unicode -from test import clear_warning_registry, unittest +from test import unittest class TestURI(unittest.TestCase): @@ -435,14 +434,6 @@ class TestURI(unittest.TestCase): "mongodb://user%40domain.com:password" "@localhost/foo?uuidrepresentation=notAnOption") - def test_parse_uri_unicode(self): - # Ensure parsing a unicode returns option names that can be passed - # as kwargs. In Python 2.4, keyword argument names must be ASCII. - # In all Pythons, str is the type of valid keyword arg names. - res = parse_uri(_unicode("mongodb://localhost/?fsync=true")) - for key in res['options']: - self.assertTrue(isinstance(key, str)) - def test_parse_ssl_paths(self): # Turn off "validate" since these paths don't exist on filesystem. self.assertEqual( @@ -467,50 +458,29 @@ class TestURI(unittest.TestCase): 'mongodb://jesse:foo%2Fbar@%2FMongoDB.sock/?ssl_certfile=a/b', validate=False)) - def test_parse_tls_insecure_options(self): - # tlsInsecure is expanded correctly. + def test_tlsinsecure_simple(self): + # check that tlsInsecure is expanded correctly. uri = "mongodb://example.com/?tlsInsecure=true" - res = get_validated_options( - {"ssl_match_hostname": False, "ssl_cert_reqs": ssl.CERT_NONE, - "tlsinsecure": True}, warn=False) + res = { + "ssl_match_hostname": False, "ssl_cert_reqs": ssl.CERT_NONE, + "tlsinsecure": True} self.assertEqual(res, parse_uri(uri)["options"]) - # tlsAllow* specified AFTER tlsInsecure. - # tlsAllow* options warns and overrides values implied by tlsInsecure. - uri = ("mongodb://example.com/?tlsInsecure=true" - "&tlsAllowInvalidCertificates=false" - "&tlsAllowInvalidHostnames=false") - res = get_validated_options( - {"ssl_match_hostname": True, "ssl_cert_reqs": ssl.CERT_REQUIRED, - "tlsinsecure": True}, warn=False) - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('always') - self.assertEqual(res, parse_uri(uri)["options"]) - for warning in ctx: - self.assertRegexpMatches( - warning.message.args[0], - ".*tlsAllowInvalid.*overrides.*tlsInsecure.*") - clear_warning_registry() - - # tlsAllow* specified BEFORE tlsInsecure. - # tlsAllow* options warns and overrides values implied by tlsInsecure. - uri = ("mongodb://example.com/" - "?tlsAllowInvalidCertificates=false" - "&tlsAllowInvalidHostnames=false" - "&tlsInsecure=true") - res = get_validated_options( - {"ssl_match_hostname": True, "ssl_cert_reqs": ssl.CERT_REQUIRED, - "tlsinsecure": True}, warn=False) - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('always') - self.assertEqual(res, parse_uri(uri)["options"]) - for warning in ctx: - self.assertRegexpMatches( - warning.message.args[0], - ".*tlsAllowInvalid.*overrides.*tlsInsecure.*") - - + def test_tlsinsecure_legacy_conflict(self): + # must not allow use of tlsinsecure alongside legacy TLS options. + # same check for modern TLS options is performed in the spec-tests. + uri = "mongodb://srv.com/?tlsInsecure=true&ssl_match_hostname=true" + with self.assertRaises(InvalidURI): + parse_uri(uri, validate=False, warn=False, normalize=False) + def test_normalize_options(self): + # check that options are converted to their internal names correctly. + uri = ("mongodb://example.com/?tls=true&appname=myapp&maxPoolSize=10&" + "fsync=true&wtimeout=10") + res = { + "ssl": True, "appname": "myapp", "maxpoolsize": 10, + "fsync": True, "wtimeoutms": 10} + self.assertEqual(res, parse_uri(uri)["options"]) if __name__ == "__main__": diff --git a/test/test_uri_spec.py b/test/test_uri_spec.py index 6d3823410..9bc7cb178 100644 --- a/test/test_uri_spec.py +++ b/test/test_uri_spec.py @@ -121,7 +121,7 @@ def create_test(test, test_workdir): # Compare URI options. err_msg = "For option %s expected %s but got %s" - if test['options'] is not None: + if test['options']: opts = options['options'] for opt in test['options']: lopt = opt.lower() diff --git a/test/uri_options/tls-options.json b/test/uri_options/tls-options.json index d535a23f5..6db80ed62 100644 --- a/test/uri_options/tls-options.json +++ b/test/uri_options/tls-options.json @@ -84,19 +84,145 @@ "options": {} }, { - "description": "tlsInsecure=true and tlsAllowInvalidCertificates=false warns", - "uri": "mongodb://example.com/?tlsInsecure=true&tlsAllowInvalidCertificates=false", - "valid": true, - "warning": true, + "description": "tlsInsecure and tlsAllowInvalidCertificates both present (and true) raises an error", + "uri": "mongodb://example.com/?tlsInsecure=true&tlsAllowInvalidCertificates=true", + "valid": false, + "warning": false, "hosts": null, "auth": null, "options": {} }, { - "description": "tlsInsecure=true and tlsAllowInvalidHostnames=false warns", - "uri": "mongodb://example.com/?tlsInsecure=true&tlsAllowInvalidHostnames=false", + "description": "tlsInsecure and tlsAllowInvalidCertificates both present (and false) raises an error", + "uri": "mongodb://example.com/?tlsInsecure=false&tlsAllowInvalidCertificates=false", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsAllowInvalidCertificates and tlsInsecure both present (and true) raises an error", + "uri": "mongodb://example.com/?tlsAllowInvalidCertificates=true&tlsInsecure=true", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsAllowInvalidCertificates and tlsInsecure both present (and false) raises an error", + "uri": "mongodb://example.com/?tlsAllowInvalidCertificates=false&tlsInsecure=false", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsInsecure and tlsAllowInvalidHostnames both present (and true) raises an error", + "uri": "mongodb://example.com/?tlsInsecure=true&tlsAllowInvalidHostnames=true", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsInsecure and tlsAllowInvalidHostnames both present (and false) raises an error", + "uri": "mongodb://example.com/?tlsInsecure=false&tlsAllowInvalidHostnames=false", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsAllowInvalidHostnames and tlsInsecure both present (and true) raises an error", + "uri": "mongodb://example.com/?tlsAllowInvalidHostnames=true&tlsInsecure=true", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsAllowInvalidHostnames and tlsInsecure both present (and false) raises an error", + "uri": "mongodb://example.com/?tlsAllowInvalidHostnames=false&tlsInsecure=false", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tls=true and ssl=true doesn't warn", + "uri": "mongodb://example.com/?tls=true&ssl=true", "valid": true, - "warning": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tls=false and ssl=false doesn't warn", + "uri": "mongodb://example.com/?tls=false&ssl=false", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "ssl=true and tls=true doesn't warn", + "uri": "mongodb://example.com/?ssl=true&tls=true", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "ssl=false and tls=false doesn't warn", + "uri": "mongodb://example.com/?ssl=false&tls=false", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tls=false and ssl=true raises error", + "uri": "mongodb://example.com/?tls=false&ssl=true", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tls=true and ssl=false raises error", + "uri": "mongodb://example.com/?tls=true&ssl=false", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "ssl=false and tls=true raises error", + "uri": "mongodb://example.com/?ssl=false&tls=true", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "ssl=true and tls=false raises error", + "uri": "mongodb://example.com/?ssl=true&tls=false", + "valid": false, + "warning": false, "hosts": null, "auth": null, "options": {}