diff --git a/pymongo/client_options.py b/pymongo/client_options.py index d5d27b0c1..0269f7072 100644 --- a/pymongo/client_options.py +++ b/pymongo/client_options.py @@ -107,8 +107,6 @@ class ClientOptions(object): def __init__(self, username, password, database, options): self.__options = options - options = dict([validate(opt, val) for opt, val in iteritems(options)]) - self.__codec_options = _parse_codec_options(options) self.__credentials = _parse_credentials( username, password, database, options) diff --git a/pymongo/common.py b/pymongo/common.py index cdab7116b..9e9df7c5f 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -16,11 +16,12 @@ """Functions and classes common to multiple pymongo modules.""" import collections +import warnings from bson.binary import (STANDARD, PYTHON_LEGACY, JAVA_LEGACY, CSHARP_LEGACY) from bson.codec_options import CodecOptions -from bson.py3compat import string_type, integer_types +from bson.py3compat import string_type, integer_types, iteritems from pymongo.auth import MECHANISMS from pymongo.errors import ConfigurationError from pymongo.read_preferences import (read_pref_mode_from_name, @@ -74,6 +75,7 @@ COMMAND_NOT_FOUND_CODES = (59, 13390, None) # Error codes to ignore if GridFS calls createIndex on a secondary UNAUTHORIZED_CODES = (13, 16547, 16548) + def partition_node(node): """Split a host:port string into (host, int(port)) pair.""" host = node @@ -340,15 +342,16 @@ def validate_auth_mechanism_properties(option, value): for opt in value.split(','): try: key, val = opt.split(':') - if key not in _MECHANISM_PROPS: - raise ValueError("%s is not a supported auth " - "mechanism property. Must be one of " - "%s." % (key, tuple(_MECHANISM_PROPS))) - props[key] = val except ValueError: raise ValueError("auth mechanism properties must be " "key:value pairs like SERVICE_NAME:" "mongodb, not %s." % (opt,)) + if key not in _MECHANISM_PROPS: + raise ValueError("%s is not a supported auth " + "mechanism property. Must be one of " + "%s." % (key, tuple(_MECHANISM_PROPS))) + props[key] = val + return props @@ -456,6 +459,23 @@ def validate(option, value): return lower, value +def get_validated_options(options): + """Validate each entry in options and raise a warning if it is not valid. + Returns a copy of options with invalid entries removed + """ + validated_options = {} + for opt, value in iteritems(options): + lower = opt.lower() + try: + validator = VALIDATORS.get(lower, raise_config_error) + value = validator(opt, value) + except (ValueError, ConfigurationError) as exc: + warnings.warn(str(exc)) + else: + validated_options[lower] = value + return validated_options + + WRITE_CONCERN_OPTIONS = frozenset([ 'w', 'wtimeout', @@ -516,4 +536,3 @@ class BaseObject(object): The :attr:`read_preference` attribute is now read only. """ return self.__read_preference - diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index 2b9180c4c..e4bc74c0a 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -300,7 +300,7 @@ class MongoClient(common.BaseObject): for entity in host: if "://" in entity: if entity.startswith("mongodb://"): - res = uri_parser.parse_uri(entity, port, False) + res = uri_parser.parse_uri(entity, port, warn=True) seeds.update(res["nodelist"]) username = res["username"] or username password = res["password"] or password @@ -321,10 +321,14 @@ class MongoClient(common.BaseObject): monitor_class = kwargs.pop('_monitor_class', None) condition_class = kwargs.pop('_condition_class', None) - opts['document_class'] = document_class - opts['tz_aware'] = tz_aware - opts['connect'] = connect - opts.update(kwargs) + keyword_opts = kwargs + keyword_opts['document_class'] = document_class + keyword_opts['tz_aware'] = tz_aware + keyword_opts['connect'] = connect + # Validate all keyword options. + keyword_opts = dict(common.validate(k, v) + for k, v in keyword_opts.items()) + opts.update(keyword_opts) self.__options = options = ClientOptions( username, password, dbase, opts) @@ -792,6 +796,9 @@ class MongoClient(common.BaseObject): else: return 'document_class=%s.%s' % (value.__module__, value.__name__) + if "ms" in option: + return "%s='%s'" % (option, int(value * 1000)) + return '%s=%r' % (option, value) # Host first... diff --git a/pymongo/uri_parser.py b/pymongo/uri_parser.py index f1c930d62..8862fd50e 100644 --- a/pymongo/uri_parser.py +++ b/pymongo/uri_parser.py @@ -14,6 +14,7 @@ """Tools to parse and validate a MongoDB URI.""" +import warnings from bson.py3compat import PY3, iteritems, string_type @@ -22,7 +23,7 @@ if PY3: else: from urllib import unquote_plus -from pymongo.common import validate as _validate +from pymongo.common import (validate as _validate, get_validated_options) from pymongo.errors import ConfigurationError, InvalidURI @@ -129,6 +130,8 @@ def parse_host(entity, default_port=DEFAULT_PORT): port = default_port if entity[0] == '[': host, port = parse_ipv6_literal_host(entity, default_port) + elif entity.endswith(".sock"): + return entity, default_port elif entity.find(':') != -1: if entity.count(':') > 1: raise ValueError("Reserved characters such as ':' must be " @@ -137,8 +140,9 @@ def parse_host(entity, default_port=DEFAULT_PORT): "and ']' according to RFC 2732.") host, port = host.split(':', 1) if isinstance(port, string_type): - if not port.isdigit(): - raise ValueError("Port number must be an integer.") + if not port.isdigit() or int(port) > 65535 or int(port) <= 0: + raise ValueError("Port must be an integer between 0 and 65535: %s" + % (port,)) port = int(port) # Normalize hostname to lowercase, since DNS is case-insensitive: @@ -148,15 +152,23 @@ def parse_host(entity, default_port=DEFAULT_PORT): return host.lower(), port -def validate_options(opts): +def validate_options(opts, warn=False): """Validates and normalizes options passed in a MongoDB URI. - Returns a new dictionary of validated and normalized options. + Returns a new dictionary of validated and normalized options. If warn is + False then errors will be thrown for invalid options, otherwise they will + be ignored and a warning will be issued. :Parameters: - `opts`: A dict of MongoDB URI options. + - `warn` (optional): If ``True`` then warnigns will be logged and + invalid options will be ignored. Otherwise invalid options will + cause errors. """ - return dict([_validate(opt, val) for opt, val in iteritems(opts)]) + if warn: + return get_validated_options(opts) + else: + return dict([_validate(opt, val) for opt, val in iteritems(opts)]) def _parse_options(opts, delim): @@ -172,11 +184,21 @@ def _parse_options(opts, delim): # str(option) to ensure that a unicode URI results in plain 'str' # option names. 'normalized' is then suitable to be passed as # kwargs in all Python versions. - options[str(key)] = val + if str(key) in options: + warnings.warn("Duplicate URI option %s" % (str(key),)) + options[str(key)] = unquote_plus(val) + + # Special case for deprecated options + if "wtimeout" in options: + if "wtimeoutMS" in options: + options.pop("wtimeout") + warnings.warn("Option wtimeout is deprecated, use 'wtimeoutMS'" + " instead") + return options -def split_options(opts, validate=True): +def split_options(opts, validate=True, warn=False): """Takes the options portion of a MongoDB URI, validates each option and returns the options in a dictionary. @@ -202,7 +224,7 @@ def split_options(opts, validate=True): raise InvalidURI("MongoDB URI options are key=value pairs.") if validate: - return validate_options(options) + return validate_options(options, warn) return options @@ -232,7 +254,7 @@ def split_hosts(hosts, default_port=DEFAULT_PORT): return nodes -def parse_uri(uri, default_port=DEFAULT_PORT, validate=True): +def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False): """Parse and validate a MongoDB URI. Returns a dict of the form:: @@ -252,6 +274,13 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True): for a host in the URI. - `validate`: If ``True`` (the default), validate and normalize all options. + - `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. + + .. versionchanged:: 3.1 + ``warn`` added so invalid options can be ignored. """ if not uri.startswith(SCHEME): raise InvalidURI("Invalid URI scheme: URI " @@ -262,7 +291,6 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True): if not scheme_free: raise InvalidURI("Must provide at least one hostname or IP.") - nodes = None user = None passwd = None dbase = None @@ -272,11 +300,14 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True): # Check for unix domain sockets in the uri if '.sock' in scheme_free: host_part, _, path_part = _rpartition(scheme_free, '/') - try: - parse_uri('%s%s' % (SCHEME, host_part)) - except (ConfigurationError, InvalidURI): - host_part = scheme_free + if not host_part: + host_part = path_part path_part = "" + if '/' in host_part: + raise InvalidURI("Any '/' in a unix domain socket must be" + " URL encoded: %s" % host_part) + host_part = unquote_plus(host_part) + path_part = unquote_plus(path_part) else: host_part, _, path_part = _partition(scheme_free, '/') @@ -302,7 +333,12 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True): dbase, collection = dbase.split('.', 1) if opts: - options = split_options(opts, validate) + options = split_options(opts, validate, warn) + + if dbase is not None: + dbase = unquote_plus(dbase) + if collection is not None: + collection = unquote_plus(collection) return { 'nodelist': nodes, diff --git a/test/connection_string/test/invalid-uris.json b/test/connection_string/test/invalid-uris.json new file mode 100644 index 000000000..e47a52a92 --- /dev/null +++ b/test/connection_string/test/invalid-uris.json @@ -0,0 +1,220 @@ +{ + "tests": [ + { + "auth": null, + "description": "Empty string", + "hosts": null, + "options": null, + "uri": "", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid scheme", + "hosts": null, + "options": null, + "uri": "mongo://localhost:27017", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Missing host", + "hosts": null, + "options": null, + "uri": "mongodb://", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier", + "hosts": null, + "options": null, + "uri": "mongodb://localhost::27017", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier and trailing slash", + "hosts": null, + "options": null, + "uri": "mongodb://localhost::27017/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier with missing host and port", + "hosts": null, + "options": null, + "uri": "mongodb://::", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier with missing port", + "hosts": null, + "options": null, + "uri": "mongodb://localhost,localhost::", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Double colon in host identifier and second host", + "hosts": null, + "options": null, + "uri": "mongodb://localhost::27017,abc", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (negative number) with hostname", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:-1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (zero) with hostname", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:0/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (positive number) with hostname", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:65536", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (positive number) with hostname and trailing slash", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:65536/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (non-numeric string) with hostname", + "hosts": null, + "options": null, + "uri": "mongodb://localhost:foo", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (negative number) with IP literal", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:-1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (zero) with IP literal", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:0/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (positive number) with IP literal", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:65536", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (positive number) with IP literal and trailing slash", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:65536/", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Invalid port (non-numeric string) with IP literal", + "hosts": null, + "options": null, + "uri": "mongodb://[::1]:foo", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Missing delimiting slash between hosts and options", + "hosts": null, + "options": null, + "uri": "mongodb://example.com?w=1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Incomplete key value pair for option", + "hosts": null, + "options": null, + "uri": "mongodb://example.com/?w", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username with password containing an unescaped colon", + "hosts": null, + "options": null, + "uri": "mongodb://alice:foo:bar@127.0.0.1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username containing an unescaped at-sign", + "hosts": null, + "options": null, + "uri": "mongodb://alice@@127.0.0.1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Username with password containing an unescaped at-sign", + "hosts": null, + "options": null, + "uri": "mongodb://alice@foo:bar@127.0.0.1", + "valid": false, + "warning": null + }, + { + "auth": null, + "description": "Host with unescaped slash", + "hosts": null, + "options": null, + "uri": "mongodb:///tmp/mongodb-27017.sock/", + "valid": false, + "warning": null + } + ] +} diff --git a/test/connection_string/test/valid-auth.json b/test/connection_string/test/valid-auth.json new file mode 100644 index 000000000..b3aa7f9cf --- /dev/null +++ b/test/connection_string/test/valid-auth.json @@ -0,0 +1,330 @@ +{ + "tests": [ + { + "auth": { + "db": null, + "password": "foo", + "username": "alice" + }, + "description": "User info for single IPv4 host without database", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://alice:foo@127.0.0.1", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "test", + "password": "foo", + "username": "alice" + }, + "description": "User info for single IPv4 host with database", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://alice:foo@127.0.0.1/test", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "t\u0000est", + "password": "f\u0000oo", + "username": "a\u0000lice" + }, + "description": "User info for single IPv4 host with database (escaped null bytes)", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://a%00lice:f%00oo@127.0.0.1/t%00est", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "bar", + "username": "bob" + }, + "description": "User info for single IP literal host without database", + "hosts": [ + { + "host": "::1", + "port": 27018, + "type": "ip_literal" + } + ], + "options": null, + "uri": "mongodb://bob:bar@[::1]:27018", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "User info for single IP literal host with database", + "hosts": [ + { + "host": "::1", + "port": 27018, + "type": "ip_literal" + } + ], + "options": null, + "uri": "mongodb://bob:bar@[::1]:27018/admin", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "baz", + "username": "eve" + }, + "description": "User info for single hostname without database", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://eve:baz@example.com", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "db2", + "password": "baz", + "username": "eve" + }, + "description": "User info for single hostname with database", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://eve:baz@example.com/db2", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "secret", + "username": "alice" + }, + "description": "User info for multiple hosts without database", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + }, + { + "host": "example.com", + "port": 27018, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://alice:secret@127.0.0.1,example.com:27018", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "secret", + "username": "alice" + }, + "description": "User info for multiple hosts with database", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + }, + { + "host": "::1", + "port": 27019, + "type": "ip_literal" + } + ], + "options": null, + "uri": "mongodb://alice:secret@example.com,[::1]:27019/admin", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": null, + "username": "alice" + }, + "description": "Username without password", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://alice@127.0.0.1", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "", + "username": "alice" + }, + "description": "Username with empty password", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://alice:@127.0.0.1", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "my=db", + "password": null, + "username": "@l:ce" + }, + "description": "Escaped username and database without password", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://%40l%3Ace@example.com/my%3Ddb", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin?", + "password": "f:zzb@zz", + "username": "$am" + }, + "description": "Escaped user info and database (MONGODB-CR)", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": { + "authmechanism": "MONGODB-CR" + }, + "uri": "mongodb://%24am:f%3Azzb%40zz@127.0.0.1/admin%3F?authMechanism=MONGODB-CR", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": null, + "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry" + }, + "description": "Escaped username (MONGODB-X509)", + "hosts": [ + { + "host": "localhost", + "port": null, + "type": "hostname" + } + ], + "options": { + "authmechanism": "MONGODB-X509" + }, + "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509", + "valid": true, + "warning": false + }, + { + "auth": { + "db": null, + "password": "secret", + "username": "user@EXAMPLE.COM" + }, + "description": "Escaped username (GSSAPI)", + "hosts": [ + { + "host": "localhost", + "port": null, + "type": "hostname" + } + ], + "options": { + "authmechanism": "GSSAPI", + "authmechanismproperties": { + "CANONICALIZE_HOST_NAME": true, + "SERVICE_NAME": "other" + } + }, + "uri": "mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true&authMechanism=GSSAPI", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "secret", + "username": "alice" + }, + "description": "At-signs in options aren't part of the userinfo", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": { + "replicaset": "my@replicaset" + }, + "uri": "mongodb://alice:secret@example.com/admin?replicaset=my@replicaset", + "valid": true, + "warning": false + } + ] +} diff --git a/test/connection_string/test/valid-host_identifiers.json b/test/connection_string/test/valid-host_identifiers.json new file mode 100644 index 000000000..0a69dd97d --- /dev/null +++ b/test/connection_string/test/valid-host_identifiers.json @@ -0,0 +1,154 @@ +{ + "tests": [ + { + "auth": null, + "description": "Single IPv4 host without port", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://127.0.0.1", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Single IPv4 host with port", + "hosts": [ + { + "host": "127.0.0.1", + "port": 27018, + "type": "ipv4" + } + ], + "options": null, + "uri": "mongodb://127.0.0.1:27018", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Single IP literal host without port", + "hosts": [ + { + "host": "::1", + "port": null, + "type": "ip_literal" + } + ], + "options": null, + "uri": "mongodb://[::1]", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Single IP literal host with port", + "hosts": [ + { + "host": "::1", + "port": 27019, + "type": "ip_literal" + } + ], + "options": null, + "uri": "mongodb://[::1]:27019", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Single hostname without port", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://example.com", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Single hostname with port", + "hosts": [ + { + "host": "example.com", + "port": 27020, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://example.com:27020", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Single hostname (resembling IPv4) without port", + "hosts": [ + { + "host": "256.0.0.1", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://256.0.0.1", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (mixed formats)", + "hosts": [ + { + "host": "127.0.0.1", + "port": null, + "type": "ipv4" + }, + { + "host": "::1", + "port": 27018, + "type": "ip_literal" + }, + { + "host": "example.com", + "port": 27019, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://127.0.0.1,[::1]:27018,example.com:27019", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "UTF-8 hosts", + "hosts": [ + { + "host": "b\u00fccher.example.com", + "port": null, + "type": "hostname" + }, + { + "host": "uml\u00e4ut.example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://b\u00fccher.example.com,uml\u00e4ut.example.com/", + "valid": true, + "warning": false + } + ] +} diff --git a/test/connection_string/test/valid-options.json b/test/connection_string/test/valid-options.json new file mode 100644 index 000000000..01f44bce5 --- /dev/null +++ b/test/connection_string/test/valid-options.json @@ -0,0 +1,42 @@ +{ + "tests": [ + { + "auth": { + "db": "admin", + "password": "secret", + "username": "alice" + }, + "description": "Option names are normalized to lowercase", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": { + "authmechanism": "MONGODB-CR" + }, + "uri": "mongodb://alice:secret@example.com/admin?AUTHMechanism=MONGODB-CR", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Option key and value (escaped null bytes)", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": { + "replicaset": "my\u0000rs" + }, + "uri": "mongodb://example.com/?replicaSet=my%00rs", + "valid": true, + "warning": false + } + ] +} diff --git a/test/connection_string/test/valid-unix_socket-absolute.json b/test/connection_string/test/valid-unix_socket-absolute.json new file mode 100644 index 000000000..dbf5f59eb --- /dev/null +++ b/test/connection_string/test/valid-unix_socket-absolute.json @@ -0,0 +1,251 @@ +{ + "tests": [ + { + "auth": null, + "description": "Unix domain socket (absolute path with trailing slash)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock/", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket (absolute path without trailing slash)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket (absolute path with spaces in path)", + "hosts": [ + { + "host": "/tmp/ /mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2F %2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (absolute paths)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (absolute path and ipv4)", + "hosts": [ + { + "host": "127.0.0.1", + "port": 27017, + "type": "ipv4" + }, + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://127.0.0.1:27017,%2Ftmp%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (absolute path and hostname resembling relative path)", + "hosts": [ + { + "host": "mongodb-27017.sock", + "port": null, + "type": "hostname" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://mongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "foo", + "username": "alice" + }, + "description": "Unix domain socket with auth database (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://alice:foo@%2Ftmp%2Fmongodb-27017.sock/admin", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket with path resembling socket file (absolute path with trailing slash)", + "hosts": [ + { + "host": "/tmp/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fpath.to.sock%2Fmongodb-27017.sock/", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket with path resembling socket file (absolute path without trailing slash)", + "hosts": [ + { + "host": "/tmp/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fpath.to.sock%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Unix domain socket with path resembling socket file and auth (absolute path)", + "hosts": [ + { + "host": "/tmp/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://bob:bar@%2Ftmp%2Fpath.to.sock%2Fmongodb-27017.sock/admin", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin.sock", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets and auth DB resembling a socket (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin.sock", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin.shoe", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets with auth DB resembling a path (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin.shoe", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Multiple Unix domain sockets with auth and query string (absolute path)", + "hosts": [ + { + "host": "/tmp/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "w": 1 + }, + "uri": "mongodb://bob:bar@%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin?w=1", + "valid": true, + "warning": false + } + ] +} diff --git a/test/connection_string/test/valid-unix_socket-relative.json b/test/connection_string/test/valid-unix_socket-relative.json new file mode 100644 index 000000000..00b699eab --- /dev/null +++ b/test/connection_string/test/valid-unix_socket-relative.json @@ -0,0 +1,271 @@ +{ + "tests": [ + { + "auth": null, + "description": "Unix domain socket (relative path with trailing slash)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock/", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket (relative path without trailing slash)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket (relative path with spaces)", + "hosts": [ + { + "host": "rel/ /mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2F %2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (relative paths)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple Unix domain sockets (relative and absolute paths)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "/tmp/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (relative path and ipv4)", + "hosts": [ + { + "host": "127.0.0.1", + "port": 27017, + "type": "ipv4" + }, + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://127.0.0.1:27017,rel%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Multiple hosts (relative path and hostname resembling relative path)", + "hosts": [ + { + "host": "mongodb-27017.sock", + "port": null, + "type": "hostname" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://mongodb-27017.sock,rel%2Fmongodb-27018.sock", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "foo", + "username": "alice" + }, + "description": "Unix domain socket with auth database (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://alice:foo@rel%2Fmongodb-27017.sock/admin", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket with path resembling socket file (relative path with trailing slash)", + "hosts": [ + { + "host": "rel/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fpath.to.sock%2Fmongodb-27017.sock/", + "valid": true, + "warning": false + }, + { + "auth": null, + "description": "Unix domain socket with path resembling socket file (relative path without trailing slash)", + "hosts": [ + { + "host": "rel/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fpath.to.sock%2Fmongodb-27017.sock", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Unix domain socket with path resembling socket file and auth (relative path)", + "hosts": [ + { + "host": "rel/path.to.sock/mongodb-27017.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://bob:bar@rel%2Fpath.to.sock%2Fmongodb-27017.sock/admin", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin.sock", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets and auth DB resembling a socket (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin.sock", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin.shoe", + "password": null, + "username": null + }, + "description": "Multiple Unix domain sockets with auth DB resembling a path (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": null, + "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin.shoe", + "valid": true, + "warning": false + }, + { + "auth": { + "db": "admin", + "password": "bar", + "username": "bob" + }, + "description": "Multiple Unix domain sockets with auth and query string (relative path)", + "hosts": [ + { + "host": "rel/mongodb-27017.sock", + "port": null, + "type": "unix" + }, + { + "host": "rel/mongodb-27018.sock", + "port": null, + "type": "unix" + } + ], + "options": { + "w": 1 + }, + "uri": "mongodb://bob:bar@rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin?w=1", + "valid": true, + "warning": false + } + ] +} diff --git a/test/connection_string/test/valid-warnings.json b/test/connection_string/test/valid-warnings.json new file mode 100644 index 000000000..05ab214cc --- /dev/null +++ b/test/connection_string/test/valid-warnings.json @@ -0,0 +1,68 @@ +{ + "tests": [ + { + "auth": null, + "description": "Unrecognized option keys are ignored", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://example.com/?foo=bar", + "valid": true, + "warning": true + }, + { + "auth": null, + "description": "Unsupported option values are ignored", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": null, + "uri": "mongodb://example.com/?fsync=ifPossible", + "valid": true, + "warning": true + }, + { + "auth": null, + "description": "Repeated option keys", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": { + "replicaset": "test" + }, + "uri": "mongodb://example.com/?replicaSet=test&replicaSet=test", + "valid": true, + "warning": true + }, + { + "auth": null, + "description": "Deprecated (or unknown) options are ignored if replacement exists", + "hosts": [ + { + "host": "example.com", + "port": null, + "type": "hostname" + } + ], + "options": { + "wtimeoutms": 10 + }, + "uri": "mongodb://example.com/?wtimeout=5&wtimeoutMS=10", + "valid": true, + "warning": true + } + ] +} diff --git a/test/test_client.py b/test/test_client.py index 9b4b361dc..070869683 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -244,7 +244,7 @@ class TestClient(IntegrationTest): self.assertFalse(client_context.rs_or_standalone_client != c) def test_host_w_port(self): - with self.assertRaises(AutoReconnect): + with self.assertRaises(ValueError): connected(MongoClient("%s:1234567" % host, connectTimeoutMS=1, serverSelectionTimeoutMS=10)) @@ -264,8 +264,8 @@ class TestClient(IntegrationTest): "tz_aware=False, " "connect=False, ", the_repr) - self.assertIn("connectTimeoutMS='12345'", the_repr) - self.assertIn("replicaSet=", the_repr) + self.assertIn("connecttimeoutms='12345'", the_repr) + self.assertIn("replicaset=", the_repr) self.assertEqual(eval(the_repr), client) @@ -400,13 +400,14 @@ class TestClient(IntegrationTest): raise SkipTest("UNIX-sockets are not supported on this system") mongodb_socket = '/tmp/mongodb-27017.sock' + encoded_socket = '%2Ftmp%2Fmongodb-27017.sock' if not os.access(mongodb_socket, os.R_OK): raise SkipTest("Socket file is not accessible") if client_context.auth_enabled: - uri = "mongodb://%s:%s@%s" % (db_user, db_pwd, mongodb_socket) + uri = "mongodb://%s:%s@%s" % (db_user, db_pwd, encoded_socket) else: - uri = "mongodb://%s" % mongodb_socket + uri = "mongodb://%s" % encoded_socket # Confirm we can do operations via the socket. client = MongoClient(uri) @@ -417,7 +418,7 @@ class TestClient(IntegrationTest): # Confirm it fails with a missing socket. self.assertRaises( ConnectionFailure, - connected, MongoClient("mongodb:///tmp/non-existent.sock", + connected, MongoClient("mongodb://%2Ftmp%2Fnon-existent.sock", serverSelectionTimeoutMS=100)) def test_fork(self): @@ -482,7 +483,6 @@ class TestClient(IntegrationTest): self.assertEqual(SON, c.codec_options.document_class) self.assertTrue(isinstance(db.test.find_one(), SON)) - def test_timeouts(self): client = rs_or_single_client(connectTimeoutMS=10500) self.assertEqual(10.5, get_pool(client).opts.connect_timeout) @@ -547,12 +547,14 @@ class TestClient(IntegrationTest): 'mongodb://localhost/?serverSelectionTimeoutMS=0', connect=False) self.assertAlmostEqual(0, client.server_selection_timeout) - self.assertRaises(ValueError, MongoClient, - 'mongodb://localhost/?serverSelectionTimeoutMS=-1', - connect=False) - self.assertRaises(ValueError, MongoClient, - 'mongodb://localhost/?serverSelectionTimeoutMS=', - connect=False) + # Test invalid timeout in URI ignored and set to default. + client = MongoClient( + 'mongodb://localhost/?serverSelectionTimeoutMS=-1', connect=False) + self.assertAlmostEqual(30, client.server_selection_timeout) + + client = MongoClient( + 'mongodb://localhost/?serverSelectionTimeoutMS=', connect=False) + self.assertAlmostEqual(30, client.server_selection_timeout) def test_waitQueueTimeoutMS(self): client = rs_or_single_client(waitQueueTimeoutMS=2000) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 9b0787caa..9dffd2281 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -16,6 +16,7 @@ import copy import sys +import warnings sys.path[0:0] = [""] @@ -94,42 +95,94 @@ class TestURI(unittest.TestCase): def test_split_options(self): self.assertRaises(ConfigurationError, split_options, 'foo') - self.assertRaises(ConfigurationError, split_options, 'foo=bar') self.assertRaises(ConfigurationError, split_options, 'foo=bar;foo') + self.assertTrue(split_options('ssl=true')) + self.assertTrue(split_options('ssl_match_hostname=true')) + + # Test Invalid URI options that should throw warnings. + with warnings.catch_warnings(): + warnings.filterwarnings('error') + self.assertRaises(Warning, split_options, + 'foo=bar', warn=True) + self.assertRaises(Warning, split_options, + 'socketTimeoutMS=foo', warn=True) + self.assertRaises(Warning, split_options, + 'socketTimeoutMS=0.0', warn=True) + self.assertRaises(Warning, split_options, + 'connectTimeoutMS=foo', warn=True) + self.assertRaises(Warning, split_options, + 'connectTimeoutMS=0.0', warn=True) + self.assertRaises(Warning, split_options, + 'connectTimeoutMS=1e100000', warn=True) + self.assertRaises(Warning, split_options, + 'connectTimeoutMS=-1e100000', warn=True) + self.assertRaises(Warning, split_options, + 'ssl=foo', warn=True) + self.assertRaises(Warning, split_options, + 'ssl_match_hostname=foo', warn=True) + + # On most platforms float('inf') and float('-inf') represent + # +/- infinity, although on Python 2.4 and 2.5 on Windows those + # expressions are invalid + if not (sys.platform == "win32" and sys.version_info <= (2, 5)): + self.assertRaises(Warning, split_options, + 'connectTimeoutMS=inf', warn=True) + self.assertRaises(Warning, split_options, + 'connectTimeoutMS=-inf', warn=True) + + self.assertRaises(Warning, split_options, 'wtimeoutms=foo', + warn=True) + self.assertRaises(Warning, split_options, 'wtimeoutms=5.5', + warn=True) + self.assertRaises(Warning, split_options, 'fsync=foo', + warn=True) + self.assertRaises(Warning, split_options, 'fsync=5.5', + warn=True) + self.assertRaises(Warning, + split_options, 'authMechanism=foo', + warn=True) + + # Test invalid options with warn=False. + self.assertRaises(ConfigurationError, split_options, 'foo=bar') self.assertRaises(ValueError, split_options, 'socketTimeoutMS=foo') self.assertRaises(ValueError, split_options, 'socketTimeoutMS=0.0') self.assertRaises(ValueError, split_options, 'connectTimeoutMS=foo') self.assertRaises(ValueError, split_options, 'connectTimeoutMS=0.0') - self.assertRaises(ValueError, split_options, 'connectTimeoutMS=1e100000') - self.assertRaises(ValueError, split_options, 'connectTimeoutMS=-1e100000') + self.assertRaises(ValueError, split_options, + 'connectTimeoutMS=1e100000') + self.assertRaises(ValueError, split_options, + 'connectTimeoutMS=-1e100000') self.assertRaises(ValueError, split_options, 'ssl=foo') - self.assertTrue(split_options('ssl=true')) self.assertRaises(ValueError, split_options, 'ssl_match_hostname=foo') - self.assertTrue(split_options('ssl_match_hostname=true')) - - # On most platforms float('inf') and float('-inf') represent - # +/- infinity, although on Python 2.4 and 2.5 on Windows those - # expressions are invalid if not (sys.platform == "win32" and sys.version_info <= (2, 5)): - self.assertRaises(ValueError, split_options, 'connectTimeoutMS=inf') - self.assertRaises(ValueError, split_options, 'connectTimeoutMS=-inf') + self.assertRaises(ValueError, split_options, + 'connectTimeoutMS=inf') + self.assertRaises(ValueError, split_options, + 'connectTimeoutMS=-inf') + self.assertRaises(ValueError, split_options, 'wtimeoutms=foo') + self.assertRaises(ValueError, split_options, 'wtimeoutms=5.5') + self.assertRaises(ValueError, split_options, 'fsync=foo') + self.assertRaises(ValueError, split_options, 'fsync=5.5') + self.assertRaises(ValueError, + split_options, 'authMechanism=foo') + # Test splitting options works when valid. self.assertTrue(split_options('socketTimeoutMS=300')) self.assertTrue(split_options('connectTimeoutMS=300')) - self.assertEqual({'sockettimeoutms': 0.3}, split_options('socketTimeoutMS=300')) - self.assertEqual({'sockettimeoutms': 0.0001}, split_options('socketTimeoutMS=0.1')) - self.assertEqual({'connecttimeoutms': 0.3}, split_options('connectTimeoutMS=300')) - self.assertEqual({'connecttimeoutms': 0.0001}, split_options('connectTimeoutMS=0.1')) + self.assertEqual({'sockettimeoutms': 0.3}, + split_options('socketTimeoutMS=300')) + self.assertEqual({'sockettimeoutms': 0.0001}, + split_options('socketTimeoutMS=0.1')) + self.assertEqual({'connecttimeoutms': 0.3}, + split_options('connectTimeoutMS=300')) + self.assertEqual({'connecttimeoutms': 0.0001}, + split_options('connectTimeoutMS=0.1')) self.assertTrue(split_options('connectTimeoutMS=300')) self.assertTrue(isinstance(split_options('w=5')['w'], int)) self.assertTrue(isinstance(split_options('w=5.5')['w'], string_type)) self.assertTrue(split_options('w=foo')) self.assertTrue(split_options('w=majority')) - self.assertRaises(ValueError, split_options, 'wtimeoutms=foo') - self.assertRaises(ValueError, split_options, 'wtimeoutms=5.5') self.assertTrue(split_options('wtimeoutms=500')) - self.assertRaises(ValueError, split_options, 'fsync=foo') - self.assertRaises(ValueError, split_options, 'fsync=5.5') self.assertEqual({'fsync': True}, split_options('fsync=true')) self.assertEqual({'fsync': False}, split_options('fsync=false')) self.assertEqual({'authmechanism': 'GSSAPI'}, @@ -138,9 +191,8 @@ class TestURI(unittest.TestCase): split_options('authMechanism=MONGODB-CR')) self.assertEqual({'authmechanism': 'SCRAM-SHA-1'}, split_options('authMechanism=SCRAM-SHA-1')) - self.assertRaises(ValueError, - split_options, 'authMechanism=foo') - self.assertEqual({'authsource': 'foobar'}, split_options('authSource=foobar')) + self.assertEqual({'authsource': 'foobar'}, + split_options('authSource=foobar')) self.assertEqual({'maxpoolsize': 50}, split_options('maxpoolsize=50')) def test_parse_uri(self): @@ -204,16 +256,17 @@ class TestURI(unittest.TestCase): parse_uri("mongodb://example1.com:27017,example2.com" ":27017/test.yield_historical.in")) - res = copy.deepcopy(orig) - res['nodelist'] = [("/tmp/mongodb-27017.sock", None)] - self.assertEqual(res, parse_uri("mongodb:///tmp/mongodb-27017.sock")) + # Test socket path without escaped characters. + self.assertRaises(InvalidURI, parse_uri, + "mongodb:///tmp/mongodb-27017.sock") + # Test with escaped characters. res = copy.deepcopy(orig) res['nodelist'] = [("example2.com", 27017), ("/tmp/mongodb-27017.sock", None)] self.assertEqual(res, parse_uri("mongodb://example2.com," - "/tmp/mongodb-27017.sock")) + "%2Ftmp%2Fmongodb-27017.sock")) res = copy.deepcopy(orig) res['nodelist'] = [("shoe.sock.pants.co.uk", 27017), @@ -221,14 +274,14 @@ class TestURI(unittest.TestCase): res['database'] = "nethers_db" self.assertEqual(res, parse_uri("mongodb://shoe.sock.pants.co.uk," - "/tmp/mongodb-27017.sock/nethers_db")) + "%2Ftmp%2Fmongodb-27017.sock/nethers_db")) res = copy.deepcopy(orig) res['nodelist'] = [("/tmp/mongodb-27017.sock", None), ("example2.com", 27017)] res.update({'database': 'test', 'collection': 'yield_historical.in'}) self.assertEqual(res, - parse_uri("mongodb:///tmp/mongodb-27017.sock," + parse_uri("mongodb://%2Ftmp%2Fmongodb-27017.sock," "example2.com:27017" "/test.yield_historical.in")) @@ -237,9 +290,9 @@ class TestURI(unittest.TestCase): ("example2.com", 27017)] res.update({'database': 'test', 'collection': 'yield_historical.sock'}) self.assertEqual(res, - parse_uri("mongodb:///tmp/mongodb-27017.sock," - "example2.com:27017" - "/test.yield_historical.sock")) + parse_uri("mongodb://%2Ftmp%2Fmongodb-27017.sock," + "example2.com:27017/test.yield_historical" + ".sock")) res = copy.deepcopy(orig) res['nodelist'] = [("example2.com", 27017)] @@ -252,7 +305,7 @@ class TestURI(unittest.TestCase): res['nodelist'] = [("/tmp/mongodb-27017.sock", None)] res.update({'database': 'test', 'collection': 'mongodb-27017.sock'}) self.assertEqual(res, - parse_uri("mongodb:///tmp/mongodb-27017.sock" + parse_uri("mongodb://%2Ftmp%2Fmongodb-27017.sock" "/test.mongodb-27017.sock")) res = copy.deepcopy(orig) @@ -261,8 +314,8 @@ class TestURI(unittest.TestCase): ("2001:0db8:85a3:0000:0000:8a2e:0370:7334", 27018), ("192.168.0.212", 27019), ("localhost", 27018)] - self.assertEqual(res, parse_uri("mongodb:///tmp/mongodb-27020.sock," - "[::1]:27017,[2001:0db8:" + self.assertEqual(res, parse_uri("mongodb://%2Ftmp%2Fmongodb-27020.sock" + ",[::1]:27017,[2001:0db8:" "85a3:0000:0000:8a2e:0370:7334]," "192.168.0.212:27019,localhost", 27018)) @@ -276,8 +329,8 @@ class TestURI(unittest.TestCase): res = copy.deepcopy(orig) res['options'] = {'readpreference': ReadPreference.SECONDARY.mode} - self.assertEqual(res, - parse_uri("mongodb://localhost/?readPreference=secondary")) + self.assertEqual(res, parse_uri( + "mongodb://localhost/?readPreference=secondary")) # Various authentication tests res = copy.deepcopy(orig) @@ -371,6 +424,12 @@ class TestURI(unittest.TestCase): "@localhost/foo?uuidrepresentation=" "javaLegacy")) + with warnings.catch_warnings(): + warnings.filterwarnings('error') + self.assertRaises(Warning, parse_uri, + "mongodb://user%40domain.com:password" + "@localhost/foo?uuidrepresentation=notAnOption", + warn=True) self.assertRaises(ValueError, parse_uri, "mongodb://user%40domain.com:password" "@localhost/foo?uuidrepresentation=notAnOption") diff --git a/test/test_uri_spec.py b/test/test_uri_spec.py new file mode 100644 index 000000000..f9ff7ab00 --- /dev/null +++ b/test/test_uri_spec.py @@ -0,0 +1,127 @@ +# Copyright 2011-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test the pymongo uri_parser module is up to spec.""" +import json +import os +import sys +import warnings + +sys.path[0:0] = [""] + +from pymongo.uri_parser import parse_uri +from test import unittest + +# Location of JSON test specifications. +_TEST_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + os.path.join('connection_string', 'test')) + + +class TestAllScenarios(unittest.TestCase): + pass + + +def create_test(scenario_def): + def run_scenario(self): + self.assertTrue(scenario_def['tests'], "tests cannot be empty") + for test in scenario_def['tests']: + dsc = test['description'] + + warned = False + error = False + + with warnings.catch_warnings(): + warnings.filterwarnings('error') + try: + options = parse_uri(test['uri'], warn=True) + except Warning: + warned = True + except Exception: + error = True + + self.assertEqual(not error, test['valid'], + "Test failure '%s'" % dsc) + + if test.get("warning", False): + self.assertTrue(warned, + "Expected warning for test '%s'" + % (dsc,)) + + # Redo in the case there were warnings that were not expected. + if warned: + options = parse_uri(test['uri'], warn=True) + + # Compare hosts and port. + if test['hosts'] is not None: + self.assertEqual( + len(test['hosts']), len(options['nodelist']), + "Incorrect number of hosts parsed from URI") + + for exp, actual in zip(test['hosts'], + options['nodelist']): + self.assertEqual(exp['host'], actual[0], + "Expected host %s but got %s" + % (exp['host'], actual[0])) + if exp['port'] is not None: + self.assertEqual(exp['port'], actual[1], + "Expected port %s but got %s" + % (exp['port'], actual)) + + # Compare auth options. + auth = test['auth'] + if auth is not None: + auth['database'] = auth.pop('db') # db == database + # Special case for PyMongo's collection parsing. + if options.get('collection') is not None: + options['database'] += "." + options['collection'] + for elm in auth: + if auth[elm] is not None: + self.assertEqual(auth[elm], options[elm], + "Expected %s but got %s" + % (auth[elm], options[elm])) + + # Compare URI options. + if test['options'] is not None: + for opt in test['options']: + if options.get(opt) is not None: + self.assertEqual( + options[opt], test['options'][opt], + "For option %s expected %s but got %s" + % (opt, options[opt], + test['options'][opt])) + + return run_scenario + + +def create_tests(): + for dirpath, _, filenames in os.walk(_TEST_PATH): + dirname = os.path.split(dirpath) + dirname = os.path.split(dirname[-2])[-1] + '_' + dirname[-1] + + for filename in filenames: + with open(os.path.join(dirpath, filename)) as scenario_stream: + scenario_def = json.load(scenario_stream) + # Construct test from scenario. + new_test = create_test(scenario_def) + test_name = 'test_%s_%s' % ( + dirname, os.path.splitext(filename)[0]) + new_test.__name__ = test_name + setattr(TestAllScenarios, new_test.__name__, new_test) + + +create_tests() + +if __name__ == "__main__": + unittest.main()