diff --git a/docs/tutorials/ruby-driver-create-client.txt b/docs/tutorials/ruby-driver-create-client.txt index edb04bc20b..e405177d4a 100644 --- a/docs/tutorials/ruby-driver-create-client.txt +++ b/docs/tutorials/ruby-driver-create-client.txt @@ -76,12 +76,12 @@ Since the URI options are required in camel case, which is not the Ruby standard following table shows the option in the URI and its corresponding option if passed to the constructor in Ruby. -.. note:: - +.. note:: + The options passed directly should be symbols. The options are explained in detail in the :manual:`Connection URI reference -`. +`. .. note:: Options that are set in **milliseconds** in the URI are @@ -97,8 +97,29 @@ URI Options Conversions * - URI Option - Ruby Option - * - replicaSet=String - - ``:replica_set => String`` + * - appName=String + - ``:app_name => String`` + + * - authMechanism=String + - ``:auth_mech => Symbol`` + + * - authMechanismProperties=Strings + - ``{ :auth_mech_properties => { :service_realm => String, :canonicalize_host_name => true|false, :service_name => String } }`` + + Specified as comma-separated key:value pairs, e.g. ``"SERVICE_REALM:foo,CANONICALIZE_HOSTNAME:TRUE"``. + + * - authSource=String + - ``:auth_source => String`` + + * - compressors=Strings + - ``:compressors => Array`` + + Specified as a comma-separated list. Note that the Ruby driver only supports zlib + compression; however, other drivers may support snappy. For maximum compatibility with + drivers, specify ``"snappy,zlib"``; if compatibility with other drivers is not a concern, + specify ``"zlib".`` Compression is not enabled by default and when using MongoDB 4.0 and + earlier, so zlib compression must be manually enabled on the server in order for the Ruby + driver to compress wire protocol data. * - connect=String - ``:connect => Symbol`` @@ -109,56 +130,81 @@ URI Options Conversions * - connectTimeoutMS=Integer - ``:connect_timeout => Float`` - * - socketTimeoutMS=Integer - - ``:socket_timeout => Float`` + * - fsync=Boolean + - ``{ :write => { :fsync => true|false }}`` - * - serverSelectionTimeoutMS=Integer - - ``:server_selection_timeout => Float`` + * - heartbeatFrequencyMS=Integer + - ``:heartbeat_frequency => Float`` + + * - journal=Boolean + - ``{ :write => { :j => true|false }}`` * - localThresholdMS=Integer - ``:local_threshold => Float`` + * - maxIdleTimeMS=Integer + - ``:max_idle_time => Float`` + * - maxPoolSize=Integer - ``:max_pool_size => Integer`` * - minPoolSize=Integer - ``:min_pool_size => Integer`` - * - waitQueueTimeoutMS=Integer - - ``:wait_queue_timeout => Float`` + * - readPreference=String + - ``{ :read => { :mode => Symbol }}`` - * - w=Integer|String - - ``{ :write => { :w => Integer|String }}`` + * - readPreferenceTags=Strings + - ``{ :read => { :tag_sets => Array }}`` - * - wtimeoutMS=Integer - - ``{ :write => { :wtimeout => Float }}`` + Each instance of the readPreferenceTags field is a comma-separated key:value pair which will appear in the :tag_sets array in the order they are specified. For instance, ``"readPreferenceTags=dc:ny,rack:1&readPreferenceTags=dc:ny"`` will be converted to ``[ { 'dc' => 'ny', 'rack' => '1' }, { 'dc' => 'ny' }]``. - * - journal=Boolean - - ``{ :write => { :j => true|false }}`` + * - replicaSet=String + - ``:replica_set => String`` - * - fsync=Boolean - - ``{ :write => { :fsync => true|false }}`` + * - serverSelectionTimeoutMS=Integer + - ``:server_selection_timeout => Float`` - * - readPreference=String - - ``{ :read => { :mode => Symbol }}`` + * - socketTimeoutMS=Integer + - ``:socket_timeout => Float`` - * - readPreferenceTags=Strings - - ``{ :read => { :tag_sets => Array }}`` + * - tls=Boolean + - ``:ssl => boolean`` - * - authSource=String - - ``:auth_source => String`` + * - tlsAllowInvalidCertificates=Boolean + - ``:ssl_verify => boolean`` - * - authMechanism=String - - ``:auth_mech => Symbol`` + Because ``tlsAllowInvalidCertificates`` uses ``true`` to signify that verification + should be disabled and ``ssl_verify`` uses ``false`` to signify that verification should be + disabled, the boolean is inverted before being used to set ``ssl_verify``. - * - authMechanismProperties=Strings - - ``{ :auth_mech_properties => { :service_realm => String, :canonicalize_host_name => true|false, :service_name => String } }`` + * - tlsCAFile=String + - ``:ssl_ca_cert => String`` - * - appName=String - - ``:app_name => String`` + * - tlsClientCertFile=String + - ``:ssl_cert => String`` - * - compressors=Strings - - ``:compressors => Array`` + * - tlsClientKeyFile=String + - ``:ssl_key => String`` + + * - tlsClientKeyPassword=String + - ``:ssl_key_pass_phrase => String`` + + * - tlsInsecure=Boolean + - ``:ssl_verify => boolean`` + + Because tlsInsecure uses ``true`` to signify that verification should be disabled and + ``ssl_verify`` uses ``false`` to signify that verification should be disabled, the boolean + is inverted before being used to set ``ssl_verify``. + + * - w=Integer|String + - ``{ :write => { :w => Integer|String }}`` + + * - waitQueueTimeoutMS=Integer + - ``:wait_queue_timeout => Float`` + + * - wtimeoutMS=Integer + - ``{ :write => { :wtimeout => Float }}`` * - zlibCompressionLevel=Integer - ``:zlib_compression_level => Integer`` @@ -178,12 +224,12 @@ Ruby Options * - ``:replica_set`` - When connecting to a replica set, this is the name of the set to - filter servers by. + filter servers by. - ``String`` - none * - ``:ssl`` - - Tell the client to connect to the servers via SSL. + - Tell the client to connect to the servers via SSL. - ``Boolean`` - false @@ -255,30 +301,30 @@ Ruby Options * - ``:connect_timeout`` - The number of seconds to wait to establish a socket connection - before raising an exception. + before raising an exception. - ``Float`` - 10 seconds * - ``:socket_timeout`` - The number of seconds to wait for an operation to execute on a - socket before raising an exception. + socket before raising an exception. - ``Float`` - 5 seconds * - ``:max_pool_size`` - - The maximum size of the connection pool for each server. + - The maximum size of the connection pool for each server. - ``Integer`` - 5 * - ``:min_pool_size`` - The minimum number of connections in the connection pool for each - server. + server. - ``Integer`` - 1 * - ``:wait_queue_timeout`` - The number of seconds to wait for a connection in the connection - pool to become available. + pool to become available. - ``Float`` - 1 @@ -291,22 +337,22 @@ Ruby Options { :write => { :w => 2 } } - ``Hash`` - ``{ :w => 1 }`` - + * - ``:read`` - Specifies the read preference mode and tag sets for selecting servers as a ``Hash``. Keys in the hash are ``:mode`` and ``:tag_sets``. .. code-block:: ruby - { :read => - { :mode => :secondary, - :tag_sets => [ "berlin" ] - } + { :read => + { :mode => :secondary, + :tag_sets => [ "berlin" ] + } } - ``Hash`` - ``{ :mode => :primary }`` - + * - ``:auth_source`` - Specifies the authentication source. - ``String`` @@ -316,7 +362,7 @@ Ruby Options * - ``:auth_mech`` - Specifies the authenticaion mechanism to use. Can be one of: - ``:mongodb_cr``, ``:mongodb_x509``, ``:plain``, ``:scram``. + ``:mongodb_cr``, ``:mongodb_x509``, ``:plain``, ``:scram``. - ``Symbol`` - MongoDB 3.0 and later: ``:scram`` if user credentials are supplied but an ``:auth_mech`` is not. 2.6 and earlier: @@ -343,7 +389,7 @@ Ruby Options ``:replica_set`` or ``:sharded``. - ``Symbol`` - none - + * - ``:heartbeat_frequency`` - The number of seconds for the server monitors to refresh server states asynchronously. @@ -437,9 +483,9 @@ Details on Timeout Options ``connect_timeout`` On initialization of a connection to a server, this setting is the - number of seconds to wait to connect before raising an exception. + number of seconds to wait to connect before raising an exception. This timeout is also used when monitor threads ping their servers. - The default is 10 seconds. See the `socket timeout for monitoring + The default is 10 seconds. See the `socket timeout for monitoring specification `_ for further explanation. @@ -457,7 +503,7 @@ Details on Timeout Options The number of seconds to wait for the driver to find an appropriate server to which an operation can be sent before raising an exception. Defaults to 30. It should take the speed of :manual:`elections` - during a failover into account. See the + during a failover into account. See the `serverSelectionTimeoutMS specification `_ for further information. @@ -466,10 +512,10 @@ Details on Timeout Options The maximum latency in seconds between the nearest server and the servers that can be considered available to send an operation to. Defaults to 0.015. -.. note:: +.. note:: This is not the latency window between the driver and a server, but rather the latency between the nearest server and other servers. See - `the localThresholdMS specification + `the localThresholdMS specification `_. ``wait_queue_timeout`` @@ -483,7 +529,7 @@ Details on Timeout Options ``min_pool_size`` Minimum number of connections in the connection pool for each server. - Increase this number to create connections when the pool is + Increase this number to create connections when the pool is initialized and to reduce the overhead of creating new connections later on. Defaults to 1. @@ -669,7 +715,7 @@ You can either use the default global driver logger or set your own. To set your Mongo::Logger.logger = other_logger See the `Ruby Logger documentation `_ -for more information on the default logger API and available levels. +for more information on the default logger API and available levels. Changing the Logger Level ````````````````````````` diff --git a/lib/mongo/uri.rb b/lib/mongo/uri.rb index 0e76be63bd..14c30ffa00 100644 --- a/lib/mongo/uri.rb +++ b/lib/mongo/uri.rb @@ -426,29 +426,38 @@ def self.uri_option(uri_key, name, extra = {}) uri_option 'replicaset', :replica_set, :type => :replica_set # Timeout Options - uri_option 'connecttimeoutms', :connect_timeout, :type => :ms_convert - uri_option 'sockettimeoutms', :socket_timeout, :type => :ms_convert - uri_option 'serverselectiontimeoutms', :server_selection_timeout, :type => :ms_convert - uri_option 'localthresholdms', :local_threshold, :type => :ms_convert + uri_option 'connecttimeoutms', :connect_timeout, :type => :connect_timeout + uri_option 'sockettimeoutms', :socket_timeout, :type => :socket_timeout + uri_option 'serverselectiontimeoutms', :server_selection_timeout, :type => :server_selection_timeout + uri_option 'localthresholdms', :local_threshold, :type => :local_threshold + uri_option 'heartbeatfrequencyms', :heartbeat_frequency, :type => :heartbeat_frequency + uri_option 'maxidletimems', :max_idle_time, :type => :max_idle_time # Write Options uri_option 'w', :w, :group => :write - uri_option 'journal', :j, :group => :write + uri_option 'journal', :j, :group => :write, :type => :journal uri_option 'fsync', :fsync, :group => :write - uri_option 'wtimeoutms', :timeout, :group => :write + uri_option 'wtimeoutms', :timeout, :group => :write, :type => :wtimeout # Read Options uri_option 'readpreference', :mode, :group => :read, :type => :read_mode uri_option 'readpreferencetags', :tag_sets, :group => :read, :type => :read_tags - uri_option 'maxstalenessseconds', :max_staleness, :group => :read + uri_option 'maxstalenessseconds', :max_staleness, :group => :read, :type => :max_staleness # Pool options uri_option 'minpoolsize', :min_pool_size uri_option 'maxpoolsize', :max_pool_size - uri_option 'waitqueuetimeoutms', :wait_queue_timeout, :type => :ms_convert + uri_option 'waitqueuetimeoutms', :wait_queue_timeout, :type => :wait_queue_timeout # Security Options uri_option 'ssl', :ssl + uri_option 'tls', :ssl + uri_option 'tlsallowinvalidcertificates', :ssl_verify, :type => :ssl_verify + uri_option 'tlscafile', :ssl_ca_cert + uri_option 'tlscertificatekeyfile', :ssl_cert + uri_option 'tlsclientkeyfile', :ssl_key + uri_option 'tlscertificatekeypassword', :ssl_key_pass_phrase + uri_option 'tlsinsecure', :ssl_verify, :type => :ssl_verify # Topology options uri_option 'connect', :connect @@ -461,7 +470,9 @@ def self.uri_option(uri_key, name, extra = {}) # Client Options uri_option 'appname', :app_name uri_option 'compressors', :compressors, :type => :array - uri_option 'zlibcompressionlevel', :zlib_compression_level + uri_option 'readconcernlevel', :read_concern + uri_option 'retrywrites', :retry_writes, :type => :retry_writes + uri_option 'zlibcompressionlevel', :zlib_compression_level, :type => :zlib_compression_level # Casts option values that do not have a specifically provided # transformation to the appropriate type. @@ -571,7 +582,9 @@ def auth_source(value) # # @return [Symbol] The transformed authentication mechanism. def auth_mech(value) - AUTH_MECH_MAP[value.upcase] + AUTH_MECH_MAP[value.upcase].tap do |mech| + log_warn("#{value} is not a valid auth mechanism") unless mech + end end # Read preference mode transformation. @@ -598,7 +611,7 @@ def read_tags(value) # # @return [Hash] The tag set hash. def read_set(value) - hash_extractor(value) + hash_extractor('readPreferenceTags', value) end # Auth mechanism properties extractor. @@ -607,7 +620,7 @@ def read_set(value) # # @return [ Hash ] The auth mechanism properties hash. def auth_mech_props(value) - properties = hash_extractor(value) + properties = hash_extractor('authMechanismProperties', value) if properties[:canonicalize_host_name] properties.merge!(canonicalize_host_name: properties[:canonicalize_host_name] == 'true') @@ -615,6 +628,186 @@ def auth_mech_props(value) properties end + # Parses the zlib compression level. + # + # @param value [ String ] The zlib compression level string. + # + # @return [ Integer | nil ] The compression level value if it is between -1 and 9 (inclusive), + # otherwise nil (and a warning will be logged). + def zlib_compression_level(value) + if /\A-?\d+\z/ =~ value + i = value.to_i + + if i >= -1 && i <= 9 + return i + end + end + + log_warn("#{value} is not a valid zlibCompressionLevel") + nil + end + + # Parses the journal value. + # + # @param value [ String ] The journal value. + # + # @return [ true | false | nil ] The journal value parsed out, otherwise nil (and a warning + # will be logged). + def journal(value) + bool('journal', value) + end + + # Parses the ssl_verify value. Note that this will be the inverse of the value of + # tlsAllowInvalidCertificates (if present). + # + # @param value [ String ] The tlsAllowInvalidCertificates value. + # + # @return [ true | false | nil ] The ssl_verify value parsed out, otherwise nil (and a warning + # will be logged). + def ssl_verify(value) + b = bool('tlsAllowInvalidCertificates', value) + + if b.nil? + nil + else + !b + end + end + + # Parses the retryWrites value. + # + # @param value [ String ] The retryWrites value. + # + # @return [ true | false | nil ] The boolean value parsed out, otherwise nil (and a warning + # will be logged). + def retry_writes(value) + bool('retryWrites', value) + end + + # Parses a boolean value. + # + # @param value [ String ] The URI option value. + # + # @return [ true | false | nil ] The boolean value parsed out, otherwise nil (and a warning + # will be logged). + def bool(name, value) + case value + when "true" + true + when "false" + false + else + log_warn("invalid boolean option for #{name}: #{value}") + nil + end + end + + # Parses the max staleness value, which must be either "0" or an integer greater or equal to 90. + # + # @param value [ String ] The max staleness string. + # + # @return [ Integer | nil ] The max staleness integer parsed out if it is valid, otherwise nil + # (and a warning will be logged). + def max_staleness(value) + if /\A\d+\z/ =~ value + int = value.to_i + + if int >= 0 && int < 90 + log_warn("max staleness must be either 0 or greater than 90: #{value}") + end + + return int + end + + log_warn("Invalid max staleness value: #{value}") + nil + end + + # Parses the connectTimeoutMS value. + # + # @param value [ String ] The connectTimeoutMS value. + # + # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be + # logged). + def connect_timeout(value) + ms_convert('connectTimeoutMS', value) + end + + # Parses the localThresholdMS value. + # + # @param value [ String ] The localThresholdMS value. + # + # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be + # logged). + def local_threshold(value) + ms_convert('localThresholdMS', value) + end + + # Parses the heartbeatFrequencyMS value. + # + # @param value [ String ] The heartbeatFrequencyMS value. + # + # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be + # logged). + def heartbeat_frequency(value) + ms_convert('heartbeatFrequencyMS', value) + end + + # Parses the maxIdleTimeMS value. + # + # @param value [ String ] The maxIdleTimeMS value. + # + # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be + # logged). + def max_idle_time(value) + ms_convert('maxIdleTimeMS', value) + end + + # Parses the serverSelectionMS value. + # + # @param value [ String ] The serverSelectionMS value. + # + # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be + # logged). + def server_selection_timeout(value) + ms_convert('serverSelectionTimeoutMS', value) + end + + # Parses the socketTimeoutMS value. + # + # @param value [ String ] The socketTimeoutMS value. + # + # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be + # logged). + def socket_timeout(value) + ms_convert('socketTimeoutMS', value) + end + + # Parses the waitQueueTimeoutMS value. + # + # @param value [ String ] The waitQueueTimeoutMS value. + # + # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be + # logged). + def wait_queue_timeout(value) + ms_convert('MS', value) + end + + # Parses the wtimeoutMS value. + # + # @param value [ String ] The wtimeoutMS value. + # + # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be + # logged). + def wtimeout(value) + unless /\A\d+\z/ =~ value + log_warn("Invalid wtimeoutMS value: #{value}") + return nil + end + + value.to_i + end + # Ruby's convention is to provide timeouts in seconds, not milliseconds and # to use fractions where more precision is necessary. The connection string # options are always in MS so we provide an easy conversion type. @@ -624,7 +817,17 @@ def auth_mech_props(value) # @return [ Float ] The seconds value. # # @since 2.0.0 - def ms_convert(value) + def ms_convert(name, value) + unless /\A-?\d+(\.\d+)?\z/ =~ value + log_warn("Invalid ms value for #{name}: #{value}") + return nil + end + + if value[0] == '-' + log_warn("#{name} cannot be a negative number") + return nil + end + value.to_f / 1000 end @@ -633,9 +836,14 @@ def ms_convert(value) # @param value [ String ] The string to build a hash from. # # @return [ Hash ] The hash built from the string. - def hash_extractor(value) + def hash_extractor(name, value) value.split(',').reduce({}) do |set, tag| k, v = tag.split(':') + if v.nil? + log_warn("Invalid hash value for #{name}: #{value}") + return nil + end + set.merge(decode(k).downcase.to_sym => decode(v)) end end diff --git a/spec/lite_spec_helper.rb b/spec/lite_spec_helper.rb index 2be3d80178..a2fedb6ede 100644 --- a/spec/lite_spec_helper.rb +++ b/spec/lite_spec_helper.rb @@ -9,6 +9,7 @@ RETRYABLE_WRITES_TESTS = Dir.glob("#{CURRENT_PATH}/spec_tests/data/retryable_writes/**/*.yml") COMMAND_MONITORING_TESTS = Dir.glob("#{CURRENT_PATH}/spec_tests/data/command_monitoring/**/*.yml") CONNECTION_STRING_TESTS = Dir.glob("#{CURRENT_PATH}/spec_tests/data/connection_string/*.yml") +URI_OPTIONS_TESTS = Dir.glob("#{CURRENT_PATH}/spec_tests/data/uri_options/*.yml") DNS_SEEDLIST_DISCOVERY_TESTS = Dir.glob("#{CURRENT_PATH}/spec_tests/data/dns_seedlist_discovery/*.yml") GRIDFS_TESTS = Dir.glob("#{CURRENT_PATH}/spec_tests/data/gridfs/*.yml") TRANSACTIONS_TESTS = Dir.glob("#{CURRENT_PATH}/spec_tests/data/transactions/*.yml") diff --git a/spec/spec_tests/data/uri_options/auth-options.yml b/spec/spec_tests/data/uri_options/auth-options.yml new file mode 100644 index 0000000000..f55f2c9af6 --- /dev/null +++ b/spec/spec_tests/data/uri_options/auth-options.yml @@ -0,0 +1,14 @@ +tests: + - + description: "Valid auth options are parsed correctly" + uri: "mongodb://example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true&authSource=$external" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + authmechanism: "GSSAPI" + authMechanismProperties: + SERVICE_NAME: "other" + CANONICALIZE_HOST_NAME: "true" + authSource: "$external" diff --git a/spec/spec_tests/data/uri_options/compression-options.yml b/spec/spec_tests/data/uri_options/compression-options.yml new file mode 100644 index 0000000000..2dfd19eb1e --- /dev/null +++ b/spec/spec_tests/data/uri_options/compression-options.yml @@ -0,0 +1,44 @@ +tests: + - + description: "Valid compression options are parsed correctly" + uri: "mongodb://example.com/?compressors=zlib&zlibCompressionLevel=9" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + compressors: "zlib" + zlibCompressionLevel: 9 + - + description: "Invalid compressors cause a warning" + uri: "mongodb://example.com/?compressors=jpeg" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Non-numeric zlibCompressionLevel causes a warning" + uri: "mongodb://example.com/?compressors=zlib&zlibCompressionLevel=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low zlibCompressionLevel causes a warning" + uri: "mongodb://example.com/?compressors=zlib&zlibCompressionLevel=-2" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too high zlibCompressionLevel causes a warning" + uri: "mongodb://example.com/?compressors=zlib&zlibCompressionLevel=10" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + diff --git a/spec/spec_tests/data/uri_options/concern-options.yml b/spec/spec_tests/data/uri_options/concern-options.yml new file mode 100644 index 0000000000..c883241739 --- /dev/null +++ b/spec/spec_tests/data/uri_options/concern-options.yml @@ -0,0 +1,55 @@ +tests: + - + description: "Valid read and write concern are parsed correctly" + uri: "mongodb://example.com/?readConcernLevel=majority&w=5&wTimeoutMS=30000&journal=false" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + readConcernLevel: "majority" + w: 5 + wTimeoutMS: 30000 + journal: false + - + description: "Arbitrary string readConcernLevel does not cause a warning" + uri: "mongodb://example.com/?readConcernLevel=arbitraryButStillValid" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + readConcernLevel: "arbitraryButStillValid" + - + description: "Arbitrary string w doesn't cause a warning" + uri: "mongodb://example.com/?w=arbitraryButStillValid" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + w: "arbitraryButStillValid" + - + description: "Non-numeric wTimeoutMS causes a warning" + uri: "mongodb://example.com/?wTimeoutMS=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low wTimeoutMS causes a warning" + uri: "mongodb://example.com/?wTimeoutMS=-2" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Invalid journal causes a warning" + uri: "mongodb://example.com/?journal=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} diff --git a/spec/spec_tests/data/uri_options/connection-options.yml b/spec/spec_tests/data/uri_options/connection-options.yml new file mode 100644 index 0000000000..db1ea6be36 --- /dev/null +++ b/spec/spec_tests/data/uri_options/connection-options.yml @@ -0,0 +1,105 @@ +tests: + - + description: "Valid connection and timeout options are parsed correctly" + uri: "mongodb://example.com/?appname=URI-OPTIONS-SPEC-TEST&connectTimeoutMS=20000&heartbeatFrequencyMS=5000&localThresholdMS=3000&replicaSet=uri-options-spec&retryWrites=true&serverSelectionTimeoutMS=15000&socketTimeoutMS=7500" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + appname: "URI-OPTIONS-SPEC-TEST" + connectTimeoutMS: 20000 + heartbeatFrequencyMS: 5000 + localThresholdMS: 3000 + maxIdleTimeMS: 50000 + replicaSet: "uri-options-spec" + retryWrites: true + serverSelectionTimeoutMS: 15000 + socketTimeoutMS: 7500 + - + description: "Non-numeric connectTimeoutMS causes a warning" + uri: "mongodb://example.com/?connectTimeoutMS=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low connectTimeoutMS causes a warning" + uri: "mongodb://example.com/?connectTimeoutMS=-2" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Non-numeric heartbeatFrequencyMS causes a warning" + uri: "mongodb://example.com/?heartbeatFrequencyMS=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low heartbeatFrequencyMS causes a warning" + uri: "mongodb://example.com/?heartbeatFrequencyMS=-2" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Non-numeric localThresholdMS causes a warning" + uri: "mongodb://example.com/?localThresholdMS=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low localThresholdMS causes a warning" + uri: "mongodb://example.com/?localThresholdMS=-2" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Invalid retryWrites causes a warning" + uri: "mongodb://example.com/?retryWrites=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Non-numeric serverSelectionTimeoutMS causes a warning" + uri: "mongodb://example.com/?serverSelectionTimeoutMS=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low serverSelectionTimeoutMS causes a warning" + uri: "mongodb://example.com/?serverSelectionTimeoutMS=-2" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Non-numeric socketTimeoutMS causes a warning" + uri: "mongodb://example.com/?socketTimeoutMS=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low socketTimeoutMS causes a warning" + uri: "mongodb://example.com/?socketTimeoutMS=-2" + valid: true + warning: true + hosts: ~ + auth: ~ diff --git a/spec/spec_tests/data/uri_options/connection-pool-options.yml b/spec/spec_tests/data/uri_options/connection-pool-options.yml new file mode 100644 index 0000000000..54cee220b5 --- /dev/null +++ b/spec/spec_tests/data/uri_options/connection-pool-options.yml @@ -0,0 +1,26 @@ +tests: + - + description: "Valid connection pool options are parsed correctly" + uri: "mongodb://example.com/?maxIdleTimeMS=50000" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + maxIdleTimeMS: 50000 + - + description: "Non-numeric maxIdleTimeMS causes a warning" + uri: "mongodb://example.com/?maxIdleTimeMS=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low maxIdleTimeMS causes a warning" + uri: "mongodb://example.com/?maxIdleTimeMS=-2" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} diff --git a/spec/spec_tests/data/uri_options/read-preference-options.yml b/spec/spec_tests/data/uri_options/read-preference-options.yml new file mode 100644 index 0000000000..425447aec3 --- /dev/null +++ b/spec/spec_tests/data/uri_options/read-preference-options.yml @@ -0,0 +1,39 @@ +tests: + - + description: "Valid read preference options are parsed correctly" + uri: "mongodb://example.com/?readPreference=primaryPreferred&readPreferenceTags=dc:ny,rack:1&maxStalenessSeconds=120&readPreferenceTags=dc:ny" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + readPreference: "primaryPreferred" + readPreferenceTags: + - "dc:ny,rack" + - "dc:ny" + maxStalenessSeconds: 120 + - + description: "Invalid readPreferenceTags causes a warning" + uri: "mongodb://example.com/?readPreferenceTags=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Non-numeric maxStalenessSeconds causes a warning" + uri: "mongodb://example.com/?maxStalenessSeconds=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + - + description: "Too low maxStalenessSeconds causes a warning" + uri: "mongodb://example.com/?maxStalenessSeconds=-2" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} + diff --git a/spec/spec_tests/data/uri_options/tls-options.yml b/spec/spec_tests/data/uri_options/tls-options.yml new file mode 100644 index 0000000000..fa31ab1b87 --- /dev/null +++ b/spec/spec_tests/data/uri_options/tls-options.yml @@ -0,0 +1,89 @@ +tests: + - + description: "Valid required tls options are parsed correctly" + uri: "mongodb://example.com/?tls=true&tlsCAFile=ca.pem&tlsCertificateKeyFile=cert.pem&tlsCertificateKeyPassword=hunter2" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + tls: true + tlsCAFile: "ca.pem" + tlsCertificateKeyFile: "cert.pem" + tlsCertificateKeyPassword: "hunter2" + - + description: "Invalid tlsAllowInvalidCertificates causes a warning" + uri: "mongodb://example.com/?tlsAllowInvalidCertificates=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: ~ + - + description: "tlsAllowInvalidCertificates is parsed correctly" + uri: "mongodb://example.com/?tlsAllowInvalidCertificates=true" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + tlsAllowInvalidCertificates: true + - + description: "Invalid tlsAllowInvalidCertificates causes a warning" + uri: "mongodb://example.com/?tlsAllowInvalidCertificates=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: ~ + - + description: "tlsAllowInvalidHostnames is parsed correctly" + uri: "mongodb://example.com/?tlsAllowInvalidHostnames=true" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + tlsAllowInvalidHostnames: true + - + description: "Invalid tlsAllowInvalidHostnames causes a warning" + uri: "mongodb://example.com/?tlsAllowInvalidHostnames=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: ~ + - + description: "tlsInsecure is parsed correctly" + uri: "mongodb://example.com/?tlsInsecure=true" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + tlsInsecure: true + - + description: "Invalid tlsAllowInsecure causes a warning" + uri: "mongodb://example.com/?tlsAllowInsecure=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: ~ + - + description: "tlsInsecure=true and tlsAllowInvalidCertificates=false warns" + uri: "mongodb://example.com/?tlsInsecure=true&tlsAllowInvalidCertificates=false" + valid: true + warning: true + hosts: ~ + auth: ~ + options: ~ + - + description: "tlsInsecure=true and tlsAllowInvalidHostnames=false warns" + uri: "mongodb://example.com/?tlsInsecure=true&tlsAllowInvalidHostnames=false" + valid: true + warning: true + hosts: ~ + auth: ~ + options: ~ + diff --git a/spec/spec_tests/uri_options_spec.rb b/spec/spec_tests/uri_options_spec.rb new file mode 100644 index 0000000000..da3cd8e331 --- /dev/null +++ b/spec/spec_tests/uri_options_spec.rb @@ -0,0 +1,97 @@ +require 'lite_spec_helper' + +describe 'Uri Options' do + include Mongo::ConnectionString + + URI_OPTIONS_TESTS.each do |file| + + spec = Mongo::ConnectionString::Spec.new(file) + + context(spec.description) do + + before(:all) do + + module Mongo + class Address + + private + + alias :original_create_resolver :create_resolver + def create_resolver(timeout, ssl_options) + family = (host == 'localhost') ? ::Socket::AF_INET : ::Socket::AF_UNSPEC + info = ::Socket.getaddrinfo(host, nil, family, ::Socket::SOCK_STREAM) + FAMILY_MAP[info.first[4]].new(info[3], port, host) + end + end + end + end + + after(:all) do + + module Mongo + # Return the implementations to their originals for the other + # tests in the suite. + class Address + alias :create_resolver :original_create_resolver + remove_method(:original_create_resolver) + end + end + end + + spec.tests.each do |test| + + # Skip optional tests for options the driver doesn't support. + next if test.description.start_with?('tlsAllowInvalidHostnames') + + context "#{test.description}" do + + context 'when the uri should warn', if: test.warn? do + + before do + expect(Mongo::Logger.logger).to receive(:warn) + end + + it 'warns' do + expect(test.client).to be_a(Mongo::Client) + end + end + + context 'when the uri is invalid', unless: test.valid? do + + it 'raises an error' do + expect{ + test.uri + }.to raise_exception(Mongo::Error::InvalidURI) + end + end + + context 'when the uri should not warn', if: !test.warn? do + + before do + expect(Mongo::Logger.logger).not_to receive(:warn) + end + + it 'does not raise an exception or warning' do + expect(test.client).to be_a(Mongo::Client) + end + end + + context 'when the uri is valid', if: test.valid? do + + it 'creates a client with the correct hosts' do + expect(test.client).to have_hosts(test) + end + + it 'creates a client with the correct authentication properties' do + expect(test.client).to match_auth(test) + end + + it 'creates a client with the correct options' do + expect(test.client).to match_options(test) + end + end + end + end + end + end +end diff --git a/spec/support/connection_string.rb b/spec/support/connection_string.rb index fb0e4597bc..0e40f03fd1 100644 --- a/spec/support/connection_string.rb +++ b/spec/support/connection_string.rb @@ -142,7 +142,7 @@ def warn? end def hosts - @hosts ||= @spec['hosts'].collect do |host| + @hosts ||= (@spec['hosts'] || []).collect do |host| Host.new(host) end end