diff --git a/lib/aws/s3/authentication.rb b/lib/aws/s3/authentication.rb index 47089a9..4ef73ee 100644 --- a/lib/aws/s3/authentication.rb +++ b/lib/aws/s3/authentication.rb @@ -1,24 +1,24 @@ module AWS - module S3 + module S3 # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types # of authentication and when they are used may be of interest to some. # # === Header based authentication # - # Header based authentication is achieved by setting a special Authorization header whose value + # Header based authentication is achieved by setting a special Authorization header whose value # is formatted like so: # # "AWS #{access_key_id}:#{encoded_canonical}" # # The access_key_id is the public key that is assigned by Amazon for a given account which you use when - # establishing your initial connection. The encoded_canonical is computed according to rules layed out + # establishing your initial connection. The encoded_canonical is computed according to rules layed out # by Amazon which we will describe presently. # # ==== Generating the encoded canonical string # - # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, - # a set of significant headers of the current request, and the current request path into a string. - # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical + # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, + # a set of significant headers of the current request, and the current request path into a string. + # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical # string is then base 64 encoded. # # === Query string based authentication @@ -26,43 +26,44 @@ module S3 # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: # # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" - # + # # The QueryString class is responsible for generating the appropriate parameters for authentication via the # query string. # - # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. + # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. # - # All requests made by this library use header authentication. When a query string authenticated url is needed, + # All requests made by this library use header authentication. When a query string authenticated url is needed, # the S3Object#url method will include the appropriate query string parameters. # # === Full authentication specification # # The full specification of the authentication protocol can be found at - # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html + # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html class Authentication constant :AMAZON_HEADER_PREFIX, 'x-amz-' - + # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request # header value, and in the other case key/value query string parameter pairs. class Signature < String #:nodoc: attr_reader :request, :access_key_id, :secret_access_key, :options - + def initialize(request, access_key_id, secret_access_key, options = {}) super() @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key @options = options end - + private def canonical_string options = {} options[:expires] = expires if expires? + options[:security_token] = @options[:security_token] if @options.has_key?(:security_token) CanonicalString.new(request, options) end memoized :canonical_string @@ -72,20 +73,20 @@ def encoded_canonical b64_hmac = [OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)].pack("m").strip url_encode? ? CGI.escape(b64_hmac) : b64_hmac end - + def url_encode? !@options[:url_encode].nil? end - + def expires? is_a? QueryString end - + def date request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) end end - + # Provides header authentication by computing the value of the Authorization header. More details about the # various authentication schemes can be found in the docs for its containing module, Authentication. class Header < Signature #:nodoc: @@ -94,7 +95,7 @@ def initialize(*args) self << "AWS #{access_key_id}:#{encoded_canonical}" end end - + # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature. # More details about the various authentication schemes can be found in the docs for its containing module, Authentication. class QueryString < Signature #:nodoc: @@ -104,9 +105,9 @@ def initialize(*args) options[:url_encode] = true self << build end - + private - + # Will return one of three values, in the following order of precedence: # # 1) Seconds since the epoch explicitly passed in the +:expires+ option @@ -117,17 +118,17 @@ def expires return options[:expires] if options[:expires] date.to_i + expires_in end - + def expires_in options.has_key?(:expires_in) ? Integer(options[:expires_in]) : DEFAULT_EXPIRY end - + def build "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" end end - - # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of + + # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of # data related to the given request for which it provides authentication. This data includes the request method, request headers, # and the request path. Both Header and QueryString use it to generate their signature. class CanonicalString < String #:nodoc: @@ -139,7 +140,7 @@ def default_headers def interesting_headers ['content-md5', 'content-type', 'date', amazon_header_prefix] end - + def amazon_header_prefix /^#{AMAZON_HEADER_PREFIX}/io end @@ -152,7 +153,7 @@ def query_parameters response-expires response-cache-control response-content-disposition response-content-encoding) end - + def query_parameters_for_signature(params) params.select {|k, v| query_parameters.include?(k)} end @@ -168,13 +169,13 @@ def resource_parameters end attr_reader :request, :headers - + def initialize(request, options = {}) super() @request = request @headers = {} @options = options - # "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if + # "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if # an authenticated (signed) request specifies a Host: header other than 's3.amazonaws.com'" # (from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html) request['Host'] = DEFAULT_HOST @@ -185,10 +186,11 @@ def initialize(request, options = {}) def build self << "#{request.method}\n" ensure_date_is_valid - + initialize_headers set_expiry! - + set_security_token! + headers.sort_by {|k, _| k}.each do |key, value| value = value.to_s.strip self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value) @@ -196,16 +198,20 @@ def build end self << path end - + def initialize_headers identify_interesting_headers set_default_headers end - + def set_expiry! self.headers['date'] = @options[:expires] if @options[:expires] end - + + def set_security_token! + self.headers['x-amz-security-token'] = @options[:security_token] if @options[:security_token] + end + def ensure_date_is_valid request['Date'] ||= Time.now.httpdate end @@ -249,7 +255,7 @@ def extract_significant_parameter def resource_parameter?(key) self.class.resource_parameters.include? key end - + def only_path request.path[/^[^?]*/] end diff --git a/lib/aws/s3/connection.rb b/lib/aws/s3/connection.rb index 27fed33..8e3f1cd 100644 --- a/lib/aws/s3/connection.rb +++ b/lib/aws/s3/connection.rb @@ -5,46 +5,46 @@ class << self def connect(options = {}) new(options) end - + def prepare_path(path) path = path.remove_extended unless path.valid_utf8? AWS::S3.escape_uri(path) end end - - attr_reader :access_key_id, :secret_access_key, :http, :options - - # Creates a new connection. Connections make the actual requests to S3, though these requests are usually + + attr_reader :access_key_id, :secret_access_key, :security_token, :http, :options + + # Creates a new connection. Connections make the actual requests to S3, though these requests are usually # called from subclasses of Base. - # + # # For details on establishing connections, check the Connection::Management::ClassMethods. def initialize(options = {}) @options = Options.new(options) connect end - + def request(verb, path, headers = {}, body = nil, attempts = 0, &block) - body.rewind if body.respond_to?(:rewind) unless attempts.zero? - - requester = Proc.new do + body.rewind if body.respond_to?(:rewind) unless attempts.zero? + + requester = Proc.new do path = self.class.prepare_path(path) if attempts.zero? # Only escape the path once request = request_method(verb).new(path, headers) ensure_content_type!(request) add_user_agent!(request) authenticate!(request) if body - if body.respond_to?(:read) - request.body_stream = body - else - request.body = body + if body.respond_to?(:read) + request.body_stream = body + else + request.body = body end - request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size + request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size else - request.content_length = 0 + request.content_length = 0 end http.request(request, &block) end - + if persistent? http.start unless http.started? requester.call @@ -55,11 +55,11 @@ def request(verb, path, headers = {}, body = nil, attempts = 0, &block) @http = create_connection attempts == 3 ? raise : (attempts += 1; retry) end - + def url_for(path, options = {}) authenticate = options.delete(:authenticated) # Default to true unless explicitly false - authenticate = true if authenticate.nil? + authenticate = true if authenticate.nil? path = path.valid_utf8? ? path : path.remove_extended request = request_method(:get).new(path, {}) query_string = query_string_authentication(request, options) @@ -67,18 +67,18 @@ def url_for(path, options = {}) (url << (path[/\?/] ? '&' : '?') << "#{query_string}") if authenticate end end - + def subdomain http.address[/^([^.]+).#{DEFAULT_HOST}$/, 1] end - + def persistent? options[:persistent] end - + def protocol(options = {}) # This always trumps http.use_ssl? - if options[:use_ssl] == false + if options[:use_ssl] == false 'http://' elsif options[:use_ssl] || http.use_ssl? 'https://' @@ -86,7 +86,7 @@ def protocol(options = {}) 'http://' end end - + private def extract_keys! missing_keys = [] @@ -94,15 +94,16 @@ def extract_keys! @access_key_id = extract_key[:access_key_id] @secret_access_key = extract_key[:secret_access_key] raise MissingAccessKey.new(missing_keys) unless missing_keys.empty? + @security_token = extract_key[:security_token] end - + def create_connection http = http_class.new(options[:server], options[:port]) http.use_ssl = !options[:use_ssl].nil? || options[:port] == 443 http.verify_mode = OpenSSL::SSL::VERIFY_NONE http end - + def http_class if options.connecting_through_proxy? Net::HTTP::Proxy(*options.proxy_settings) @@ -110,7 +111,7 @@ def http_class Net::HTTP end end - + def connect extract_keys! @http = create_connection @@ -124,16 +125,18 @@ def port_string def ensure_content_type!(request) request['Content-Type'] ||= 'binary/octet-stream' end - + # Just do Header authentication for now def authenticate!(request) - request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key) + options = {} + options[:security_token] = security_token unless security_token.nil? + request['Authorization'] = Authentication::Header.new(request, access_key_id, secret_access_key, options) end - + def add_user_agent!(request) request['User-Agent'] ||= "AWS::S3/#{Version}" end - + def query_string_authentication(request, options = {}) Authentication::QueryString.new(request, access_key_id, secret_access_key, options) end @@ -141,29 +144,29 @@ def query_string_authentication(request, options = {}) def request_method(verb) Net::HTTP.const_get(verb.to_s.capitalize) end - + def method_missing(method, *args, &block) options[method] || super end - + module Management #:nodoc: def self.included(base) base.cattr_accessor :connections base.connections = {} base.extend ClassMethods end - + # Manage the creation and destruction of connections for AWS::S3::Base and its subclasses. Connections are # created with establish_connection!. module ClassMethods # Creates a new connection with which to make requests to the S3 servers for the calling class. - # + # # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') # # You can set connections for every subclass of AWS::S3::Base. Once the initial connection is made on # Base, all subsequent connections will inherit whatever values you don't specify explictly. This allows you to - # customize details of the connection, such as what server the requests are made to, by just specifying one - # option. + # customize details of the connection, such as what server the requests are made to, by just specifying one + # option. # # AWS::S3::Bucket.established_connection!(:use_ssl => true) # @@ -185,23 +188,23 @@ module ClassMethods # argument is set. # * :use_ssl - Whether requests should be made over SSL. If set to true, the :port argument # will be implicitly set to 443, unless specified otherwise. Defaults to false. - # * :persistent - Whether to use a persistent connection to the server. Having this on provides around a two fold + # * :persistent - Whether to use a persistent connection to the server. Having this on provides around a two fold # performance increase but for long running processes some firewalls may find the long lived connection suspicious and close the connection. # If you run into connection errors, try setting :persistent to false. Defaults to false. # * :proxy - If you need to connect through a proxy, you can specify your proxy settings by specifying a :host, :port, :user, and :password # with the :proxy option. - # The :host setting is required if specifying a :proxy. - # + # The :host setting is required if specifying a :proxy. + # # AWS::S3::Bucket.established_connection!(:proxy => { # :host => '...', :port => 8080, :user => 'marcel', :password => 'secret' # }) def establish_connection!(options = {}) - # After you've already established the default connection, just specify + # After you've already established the default connection, just specify # the difference for subsequent connections options = default_connection.options.merge(options) if connected? connections[connection_name] = Connection.connect(options) end - + # Returns the connection for the current class, or Base's default connection if the current class does not # have its own connection. # @@ -213,12 +216,12 @@ def connection raise NoConnectionEstablished end end - + # Returns true if a connection has been made yet. def connected? !connections.empty? end - + # Removes the connection for the current class. If there is no connection for the current class, the default # connection will be removed. def disconnect(name = connection_name) @@ -227,8 +230,8 @@ def disconnect(name = connection_name) connection.http.finish if connection.persistent? connections.delete(name) end - - # Clears *all* connections, from all classes, with prejudice. + + # Clears *all* connections, from all classes, with prejudice. def disconnect! connections.each_key {|connection| disconnect(connection)} end @@ -247,10 +250,10 @@ def default_connection end end end - + class Options < Hash #:nodoc: - VALID_OPTIONS = [:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy].freeze - + VALID_OPTIONS = [:access_key_id, :secret_access_key, :security_token, :server, :port, :use_ssl, :persistent, :proxy].freeze + def initialize(options = {}) super() validate(options) @@ -261,11 +264,11 @@ def initialize(options = {}) def connecting_through_proxy? !self[:proxy].nil? end - + def proxy_settings self[:proxy].values_at(:host, :port, :user, :password) end - + private def validate(options) invalid_options = options.keys - VALID_OPTIONS diff --git a/lib/aws/s3/version.rb b/lib/aws/s3/version.rb index 92547b5..e830a08 100644 --- a/lib/aws/s3/version.rb +++ b/lib/aws/s3/version.rb @@ -3,10 +3,10 @@ module S3 module VERSION #:nodoc: MAJOR = '0' MINOR = '6' - TINY = '3' + TINY = '4' BETA = nil # Time.now.to_i.to_s end - + Version = [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY, VERSION::BETA].compact * '.' end end diff --git a/test/authentication_test.rb b/test/authentication_test.rb index eed7498..7e4637f 100644 --- a/test/authentication_test.rb +++ b/test/authentication_test.rb @@ -1,18 +1,18 @@ require File.dirname(__FILE__) + '/test_helper' -class HeaderAuthenticationTest < Test::Unit::TestCase +class HeaderAuthenticationTest < Test::Unit::TestCase def test_encoded_canonical signature = Authentication::Signature.new(request, key_id, secret) assert_equal AmazonDocExampleData::Example1.canonical_string, signature.send(:canonical_string) assert_equal AmazonDocExampleData::Example1.signature, signature.send(:encoded_canonical) end - + def test_authorization_header header = Authentication::Header.new(request, key_id, secret) assert_equal AmazonDocExampleData::Example1.canonical_string, header.send(:canonical_string) assert_equal AmazonDocExampleData::Example1.authorization_header, header end - + private def request; AmazonDocExampleData::Example1.request end def key_id ; AmazonDocExampleData::Example1.access_key_id end @@ -25,13 +25,13 @@ def test_query_string assert_equal AmazonDocExampleData::Example3.canonical_string, query_string.send(:canonical_string) assert_equal AmazonDocExampleData::Example3.query_string, query_string end - + def test_query_string_with_explicit_expiry query_string = Authentication::QueryString.new(request, key_id, secret, :expires => expires) assert_equal expires, query_string.send(:canonical_string).instance_variable_get(:@options)[:expires] assert_equal AmazonDocExampleData::Example3.query_string, query_string end - + def test_expires_in_is_coerced_to_being_an_integer_in_case_it_is_a_special_integer_proxy # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=17458&group_id=2409&atid=9356 integer_proxy = Class.new do @@ -39,17 +39,17 @@ def test_expires_in_is_coerced_to_being_an_integer_in_case_it_is_a_special_integ def initialize(integer) @integer = integer end - + def to_int integer end end - + actual_integer = 25 query_string = Authentication::QueryString.new(request, key_id, secret, :expires_in => integer_proxy.new(actual_integer)) assert_equal actual_integer, query_string.send(:expires_in) end - + private def request; AmazonDocExampleData::Example3.request end def key_id ; AmazonDocExampleData::Example3.access_key_id end @@ -57,23 +57,23 @@ def secret ; AmazonDocExampleData::Example3.secret_access_key end def expires; AmazonDocExampleData::Example3.expires end end -class CanonicalStringTest < Test::Unit::TestCase +class CanonicalStringTest < Test::Unit::TestCase def setup @request = Net::HTTP::Post.new('/test') @canonical_string = Authentication::CanonicalString.new(@request) end - + def test_path_does_not_include_query_string request = Net::HTTP::Get.new('/test/query/string?foo=bar&baz=quux') assert_equal '/test/query/string', Authentication::CanonicalString.new(request).send(:path) - + # Make sure things still work when there is *no* query string request = Net::HTTP::Get.new('/') assert_equal '/', Authentication::CanonicalString.new(request).send(:path) request = Net::HTTP::Get.new('/foo/bar') assert_equal '/foo/bar', Authentication::CanonicalString.new(request).send(:path) end - + def test_path_includes_significant_query_strings significant_query_strings = [ ['/test/query/string?acl', '/test/query/string?acl'], @@ -85,29 +85,42 @@ def test_path_includes_significant_query_strings ['/test/query/string?bar=baz&acl=foo', '/test/query/string?acl'], ['/test/query/string?acl&response-content-disposition=1', '/test/query/string?acl&response-content-disposition=1'] ] - + significant_query_strings.each do |uncleaned_path, expected_cleaned_path| assert_equal expected_cleaned_path, Authentication::CanonicalString.new(Net::HTTP::Get.new(uncleaned_path)).send(:path) end end - + def test_default_headers_set Authentication::CanonicalString.default_headers.each do |header| assert @canonical_string.headers.include?(header) end end - + def test_interesting_headers_are_copied_over an_interesting_header = 'content-md5' string_without_interesting_header = Authentication::CanonicalString.new(@request) assert string_without_interesting_header.headers[an_interesting_header].empty? - + # Add an interesting header @request[an_interesting_header] = 'foo' string_with_interesting_header = Authentication::CanonicalString.new(@request) assert_equal 'foo', string_with_interesting_header.headers[an_interesting_header] end - + + def test_security_token_is_copied_over + security_token_header = 'x-amz-security-token' + string_without_security_token = Authentication::CanonicalString.new(@request) + assert !string_without_security_token.headers.has_key?(security_token_header) + + # Add the security token + string_with_security_token = Authentication::CanonicalString.new(@request, :security_token => "123456") + assert_equal '123456', string_with_security_token.headers[security_token_header] + + # Ensure the security token made a difference in the signature + assert_not_equal string_without_security_token, string_with_security_token + end + def test_canonical_string request = AmazonDocExampleData::Example1.request assert_equal AmazonDocExampleData::Example1.canonical_string, Authentication::CanonicalString.new(request) diff --git a/test/connection_test.rb b/test/connection_test.rb index 175cb12..17091d6 100644 --- a/test/connection_test.rb +++ b/test/connection_test.rb @@ -5,70 +5,70 @@ class ConnectionTest < Test::Unit::TestCase def setup @keys = {:access_key_id => '123', :secret_access_key => 'abc'}.freeze end - + def test_creating_a_connection connection = Connection.new(keys) assert_kind_of Net::HTTP, connection.http end - + def test_use_ssl_option_is_set_in_connection connection = Connection.new(keys.merge(:use_ssl => true)) assert connection.http.use_ssl? end - + def test_setting_port_to_443_implies_use_ssl connection = Connection.new(keys.merge(:port => 443)) assert connection.http.use_ssl? end - + def test_protocol connection = Connection.new(keys) assert_equal 'http://', connection.protocol connection = Connection.new(keys.merge(:use_ssl => true)) assert_equal 'https://', connection.protocol end - + def test_url_for_honors_use_ssl_option_if_it_is_false_even_if_connection_has_use_ssl_option_set # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=17628&group_id=2409&atid=9356 connection = Connection.new(keys.merge(:use_ssl => true)) assert_match %r(^http://), connection.url_for('/pathdoesnotmatter', :authenticated => false, :use_ssl => false) end - + def test_connection_is_not_persistent_by_default connection = Connection.new(keys) assert !connection.persistent? - + connection = Connection.new(keys.merge(:persistent => true)) assert connection.persistent? end - + def test_server_and_port_are_passed_onto_connection connection = Connection.new(keys) options = connection.instance_variable_get('@options') assert_equal connection.http.address, options[:server] assert_equal connection.http.port, options[:port] end - + def test_not_including_required_access_keys_raises assert_raises(MissingAccessKey) do Connection.new end - + assert_raises(MissingAccessKey) do Connection.new(:access_key_id => '123') end - + assert_nothing_raised do Connection.new(keys) end end - + def test_access_keys_extracted connection = Connection.new(keys) assert_equal '123', connection.access_key_id assert_equal 'abc', connection.secret_access_key end - + def test_request_method_class_lookup connection = Connection.new(keys) expectations = { @@ -76,7 +76,7 @@ def test_request_method_class_lookup :put => Net::HTTP::Put, :delete => Net::HTTP::Delete, :head => Net::HTTP::Head } - + expectations.each do |verb, klass| assert_equal klass, connection.send(:request_method, verb) end @@ -108,7 +108,7 @@ def test_url_for_with_canonical_query_params dispositioned = lambda {|url| url['?response-content-disposition=a']} assert dispositioned[connection.url_for("/foo?response-content-disposition=a")] end - + def test_connecting_through_a_proxy connection = nil assert_nothing_raised do @@ -116,18 +116,18 @@ def test_connecting_through_a_proxy end assert connection.http.proxy? end - + def test_request_only_escapes_the_path_the_first_time_it_runs_and_not_subsequent_times connection = Connection.new(@keys) unescaped_path = 'path with spaces' escaped_path = 'path%20with%20spaces' - + flexmock(Connection).should_receive(:prepare_path).with(unescaped_path).once.and_return(escaped_path).ordered flexmock(connection.http).should_receive(:request).and_raise(Errno::EPIPE).ordered flexmock(connection.http).should_receive(:request).ordered connection.request :put, unescaped_path end - + def test_if_request_has_no_body_then_the_content_length_is_set_to_zero # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=13052&group_id=2409&atid=9356 connection = Connection.new(@keys) @@ -138,83 +138,89 @@ def test_if_request_has_no_body_then_the_content_length_is_set_to_zero end class ConnectionOptionsTest < Test::Unit::TestCase - + def setup @options = generate_options(:server => 'example.org', :port => 555) @default_options = generate_options end - + def test_server_extracted assert_key_transfered(:server, 'example.org', @options) end - + def test_port_extracted assert_key_transfered(:port, 555, @options) end - + + def test_security_token + options = generate_options(:security_token => '12345') + assert_equal "12345", options[:security_token] + assert !@default_options.has_key?(:security_token) + end + def test_server_defaults_to_default_host assert_equal DEFAULT_HOST, @default_options[:server] end - + def test_port_defaults_to_80_if_use_ssl_is_false assert_equal 80, @default_options[:port] end - + def test_port_is_set_to_443_if_use_ssl_is_true options = generate_options(:use_ssl => true) assert_equal 443, options[:port] end - + def test_explicit_port_trumps_use_ssl options = generate_options(:port => 555, :use_ssl => true) assert_equal 555, options[:port] end - + def test_invalid_options_raise assert_raises(InvalidConnectionOption) do generate_options(:host => 'campfire.s3.amazonaws.com') end end - + def test_not_specifying_all_required_proxy_settings_raises assert_raises(ArgumentError) do generate_options(:proxy => {}) end end - + def test_not_specifying_proxy_option_at_all_does_not_raise assert_nothing_raised do generate_options end end - + def test_specifying_all_required_proxy_settings assert_nothing_raised do generate_options(:proxy => sample_proxy_settings) end end - + def test_only_host_setting_is_required assert_nothing_raised do generate_options(:proxy => {:host => 'http://google.com'}) end end - + def test_proxy_settings_are_extracted options = generate_options(:proxy => sample_proxy_settings) assert_equal sample_proxy_settings.values.map {|value| value.to_s}.sort, options.proxy_settings.map {|value| value.to_s}.sort end - + def test_recognizing_that_the_settings_want_to_connect_through_a_proxy options = generate_options(:proxy => sample_proxy_settings) assert options.connecting_through_proxy? end - + private def assert_key_transfered(key, value, options) assert_equal value, options[key] end - + def generate_options(options = {}) Connection::Options.new(options) end