diff options
Diffstat (limited to 'activeresource')
-rw-r--r-- | activeresource/lib/active_resource/base.rb | 141 | ||||
-rw-r--r-- | activeresource/lib/active_resource/connection.rb | 46 | ||||
-rw-r--r-- | activeresource/lib/active_resource/custom_methods.rb | 4 | ||||
-rw-r--r-- | activeresource/lib/active_resource/http_mock.rb | 21 | ||||
-rw-r--r-- | activeresource/test/cases/authorization_test.rb | 161 | ||||
-rw-r--r-- | activeresource/test/cases/connection_test.rb | 1 | ||||
-rw-r--r-- | activeresource/test/fixtures/street_address.rb | 2 |
7 files changed, 188 insertions, 188 deletions
diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 548990cb70..c0d51797ee 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -25,29 +25,29 @@ module ActiveResource # # == Automated mapping # - # Active Resource objects represent your RESTful resources as manipulatable Ruby objects. To map resources + # Active Resource objects represent your RESTful resources as manipulatable Ruby objects. To map resources # to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class # Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the # URI of the resources. # # class Person < ActiveResource::Base - # self.site = "http://api.people.com:3000/" + # self.site = "https://api.people.com" # end # - # Now the Person class is mapped to RESTful resources located at <tt>http://api.people.com:3000/people/</tt>, and + # Now the Person class is mapped to RESTful resources located at <tt>https://api.people.com/people/</tt>, and # you can now use Active Resource's life cycle methods to manipulate resources. In the case where you already have # an existing model with the same name as the desired RESTful resource you can set the +element_name+ value. # # class PersonResource < ActiveResource::Base - # self.site = "http://api.people.com:3000/" + # self.site = "https://api.people.com" # self.element_name = "person" # end # # If your Active Resource object is required to use an HTTP proxy you can set the +proxy+ value which holds a URI. # # class PersonResource < ActiveResource::Base - # self.site = "http://api.people.com:3000/" - # self.proxy = "http://user:password@proxy.people.com:8080" + # self.site = "https://api.people.com" + # self.proxy = "https://user:password@proxy.people.com:8080" # end # # @@ -103,7 +103,7 @@ module ActiveResource # You can validate resources client side by overriding validation methods in the base class. # # class Person < ActiveResource::Base - # self.site = "http://api.people.com:3000/" + # self.site = "https://api.people.com" # protected # def validate # errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/ @@ -114,47 +114,64 @@ module ActiveResource # # == Authentication # - # Many REST APIs will require authentication, usually in the form of basic - # HTTP authentication. Authentication can be specified by: + # Many REST APIs require authentication. The HTTP spec describes two ways to + # make requests with a username and password (see RFC 2617). # - # === HTTP Basic Authentication - # * putting the credentials in the URL for the +site+ variable. + # Basic authentication simply sends a username and password along with HTTP + # requests. These sensitive credentials are sent unencrypted, visible to + # any onlooker, so this scheme should only be used with SSL. + # + # Digest authentication sends a crytographic hash of the username, password, + # HTTP method, URI, and a single-use secret key provided by the server. + # Sensitive credentials aren't visible to onlookers, so digest authentication + # doesn't require SSL. However, this doesn't mean the connection is secure! + # Just the username and password. + # + # (You really, really want to use SSL. There's little reason not to.) + # + # === Picking an authentication scheme + # + # Basic authentication is the default. To switch to digest authentication, + # set +auth_type+ to +:digest+: # # class Person < ActiveResource::Base - # self.site = "http://ryan:password@api.people.com:3000/" + # self.auth_type = :digest # end # - # * defining +user+ and/or +password+ variables + # === Setting the username and password + # + # Set +user+ and +password+ on the class, or include them in the +site+ URL. # # class Person < ActiveResource::Base - # self.site = "http://api.people.com:3000/" + # # Set user and password directly: # self.user = "ryan" # self.password = "password" - # end - # - # For obvious security reasons, it is probably best if such services are available - # over HTTPS. # - # Note: Some values cannot be provided in the URL passed to site. e.g. email addresses - # as usernames. In those situations you should use the separate user and password option. + # # Or include them in the site: + # self.site = "https://ryan:password@api.people.com" + # end # # === Certificate Authentication # - # * End point uses an X509 certificate for authentication. <tt>See ssl_options=</tt> for all options. + # You can also authenticate using an X509 certificate. <tt>See ssl_options=</tt> for all options. # # class Person < ActiveResource::Base # self.site = "https://secure.api.people.com/" - # self.ssl_options = {:cert => OpenSSL::X509::Certificate.new(File.open(pem_file)) - # :key => OpenSSL::PKey::RSA.new(File.open(pem_file)), - # :ca_path => "/path/to/OpenSSL/formatted/CA_Certs", - # :verify_mode => OpenSSL::SSL::VERIFY_PEER} + # + # File.open(pem_file_path, 'rb') do |pem_file| + # self.ssl_options = { + # cert: OpenSSL::X509::Certificate.new(pem_file), + # key: OpenSSL::PKey::RSA.new(pem_file), + # ca_path: "/path/to/OpenSSL/formatted/CA_Certs", + # verify_mode: OpenSSL::SSL::VERIFY_PEER } + # end # end # # # == Errors & Validation # # Error handling and validation is handled in much the same manner as you're used to seeing in - # Active Record. Both the response code in the HTTP response and the body of the response are used to + # Active Record. Both the response code in the HTTP response and the body of the response are used to # indicate that an error occurred. # # === Resource errors @@ -163,7 +180,7 @@ module ActiveResource # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound # exception. # - # # GET http://api.people.com:3000/people/999.json + # # GET https://api.people.com/people/999.json # ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound # # @@ -185,7 +202,7 @@ module ActiveResource # * Other - ActiveResource::ConnectionError # # These custom exceptions allow you to deal with resource errors more naturally and with more precision - # rather than returning a general HTTP error. For example: + # rather than returning a general HTTP error. For example: # # begin # ryan = Person.find(my_id) @@ -199,7 +216,7 @@ module ActiveResource # an ActiveResource::MissingPrefixParam will be raised. # # class Comment < ActiveResource::Base - # self.site = "http://someip.com/posts/:post_id/" + # self.site = "https://someip.com/posts/:post_id" # end # # Comment.find(1) @@ -208,8 +225,8 @@ module ActiveResource # === Validation errors # # Active Resource supports validations on resources and will return errors if any of these validations fail - # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by - # a response code of <tt>422</tt> and an XML or JSON representation of the validation errors. The save operation will + # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by + # a response code of <tt>422</tt> and an XML or JSON representation of the validation errors. The save operation will # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question. # # ryan = Person.find(1) @@ -217,9 +234,9 @@ module ActiveResource # ryan.save # => false # # # When - # # PUT http://api.people.com:3000/people/1.json + # # PUT https://api.people.com/people/1.json # # or - # # PUT http://api.people.com:3000/people/1.json + # # PUT https://api.people.com/people/1.json # # is requested with invalid values, the response is: # # # # Response (422): @@ -240,7 +257,7 @@ module ActiveResource # amount of time before Active Resource times out with the +timeout+ variable. # # class Person < ActiveResource::Base - # self.site = "http://api.people.com:3000/" + # self.site = "https://api.people.com" # self.timeout = 5 # end # @@ -383,22 +400,22 @@ module ActiveResource @known_attributes ||= [] end - # Gets the URI of the REST resources to map for this class. The site variable is required for + # Gets the URI of the REST resources to map for this class. The site variable is required for # Active Resource's mapping to work. def site # Not using superclass_delegating_reader because don't want subclasses to modify superclass instance # # With superclass_delegating_reader # - # Parent.site = 'http://anonymous@test.com' - # Subclass.site # => 'http://anonymous@test.com' + # Parent.site = 'https://anonymous@test.com' + # Subclass.site # => 'https://anonymous@test.com' # Subclass.site.user = 'david' - # Parent.site # => 'http://david@test.com' + # Parent.site # => 'https://david@test.com' # # Without superclass_delegating_reader (expected behavior) # - # Parent.site = 'http://anonymous@test.com' - # Subclass.site # => 'http://anonymous@test.com' + # Parent.site = 'https://anonymous@test.com' + # Subclass.site # => 'https://anonymous@test.com' # Subclass.site.user = 'david' # => TypeError: can't modify frozen object # if defined?(@site) @@ -592,7 +609,7 @@ module ActiveResource prefix(options) end - # An attribute reader for the source string for the resource path \prefix. This + # An attribute reader for the source string for the resource path \prefix. This # method is regenerated at runtime based on what the \prefix is set to. def prefix_source prefix # generate #prefix and #prefix_source methods first @@ -625,7 +642,7 @@ module ActiveResource alias_method :set_element_name, :element_name= #:nodoc: alias_method :set_collection_name, :collection_name= #:nodoc: - # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails + # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails # will split from the \prefix options. # # ==== Options @@ -638,7 +655,7 @@ module ActiveResource # # => /posts/1.json # # class Comment < ActiveResource::Base - # self.site = "http://37s.sunrise.i/posts/:post_id/" + # self.site = "https://37s.sunrise.com/posts/:post_id" # end # # Comment.element_path(1, :post_id => 5) @@ -668,7 +685,7 @@ module ActiveResource # # => /posts/new.json # # class Comment < ActiveResource::Base - # self.site = "http://37s.sunrise.i/posts/:post_id/" + # self.site = "https://37s.sunrise.com/posts/:post_id" # end # # Comment.collection_path(:post_id => 5) @@ -677,7 +694,7 @@ module ActiveResource "#{prefix(prefix_options)}#{collection_name}/new.#{format.extension}" end - # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails + # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails # will split from the +prefix_options+. # # ==== Options @@ -725,8 +742,8 @@ module ActiveResource # ryan = Person.new(:first => 'ryan') # ryan.save # - # Returns the newly created resource. If a failure has occurred an - # exception will be raised (see <tt>save</tt>). If the resource is invalid and + # Returns the newly created resource. If a failure has occurred an + # exception will be raised (see <tt>save</tt>). If the resource is invalid and # has not been saved then <tt>valid?</tt> will return <tt>false</tt>, # while <tt>new?</tt> will still return <tt>true</tt>. # @@ -747,11 +764,11 @@ module ActiveResource self.new(attributes).tap { |resource| resource.save } end - # Core method for finding resources. Used similarly to Active Record's +find+ method. + # Core method for finding resources. Used similarly to Active Record's +find+ method. # # ==== Arguments - # The first argument is considered to be the scope of the query. That is, how many - # resources are returned from the request. It can be one of the following. + # The first argument is considered to be the scope of the query. That is, how many + # resources are returned from the request. It can be one of the following. # # * <tt>:one</tt> - Returns a single resource. # * <tt>:first</tt> - Returns the first resource found. @@ -834,7 +851,7 @@ module ActiveResource find(:last, *args) end - # This is an alias for find(:all). You can pass in all the same + # This is an alias for find(:all). You can pass in all the same # arguments to this method as you can to <tt>find(:all)</tt> def all(*args) find(:all, *args) @@ -939,12 +956,12 @@ module ActiveResource # Accepts a URI and creates the site URI from that. def create_site_uri_from(site) - site.is_a?(URI) ? site.dup : URI.parser.parse(site) + site.is_a?(URI) ? site.dup : URI.parse(site) end # Accepts a URI and creates the proxy URI from that. def create_proxy_uri_from(proxy) - proxy.is_a?(URI) ? proxy.dup : URI.parser.parse(proxy) + proxy.is_a?(URI) ? proxy.dup : URI.parse(proxy) end # contains a set of the current prefix parameters. @@ -1015,7 +1032,7 @@ module ActiveResource # not_ryan.new? # => true # # Any active resource member attributes will NOT be cloned, though all other - # attributes are. This is to prevent the conflict between any +prefix_options+ + # attributes are. This is to prevent the conflict between any +prefix_options+ # that refer to the original parent resource and the newly cloned parent # resource that does not exist. # @@ -1031,7 +1048,7 @@ module ActiveResource # Clone all attributes except the pk and any nested ARes cloned = Hash[attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.map { |k, v| [k, v.clone] }] # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which - # attempts to convert hashes into member objects and arrays into collections of objects. We want + # attempts to convert hashes into member objects and arrays into collections of objects. We want # the raw objects to be cloned so we bypass load by directly setting the attributes hash. resource = self.class.new({}) resource.prefix_options = self.prefix_options @@ -1083,7 +1100,7 @@ module ActiveResource attributes[self.class.primary_key] = id end - # Test for equality. Resource are equal if and only if +other+ is the same object or + # Test for equality. Resource are equal if and only if +other+ is the same object or # is an instance of the same class, is not <tt>new?</tt>, and has the same +id+. # # ==== Examples @@ -1139,7 +1156,7 @@ module ActiveResource end end - # Saves (+POST+) or \updates (+PUT+) a resource. Delegates to +create+ if the object is \new, + # Saves (+POST+) or \updates (+PUT+) a resource. Delegates to +create+ if the object is \new, # +update+ if it exists. If the response to the \save includes a body, it will be assumed that this body # is Json for the final object as it looked after the \save (which would include attributes like +created_at+ # that weren't part of the original submit). @@ -1190,7 +1207,7 @@ module ActiveResource end # Evaluates to <tt>true</tt> if this resource is not <tt>new?</tt> and is - # found on the remote service. Using this method, you can check for + # found on the remote service. Using this method, you can check for # resources that may have been deleted between the object's instantiation # and actions on it. # @@ -1232,7 +1249,7 @@ module ActiveResource end # A method to manually load attributes from a \hash. Recursively loads collections of - # resources. This method is called in +initialize+ and +create+ when a \hash of attributes + # resources. This method is called in +initialize+ and +create+ when a \hash of attributes # is provided. # # ==== Examples @@ -1289,12 +1306,12 @@ module ActiveResource # # Note: Unlike ActiveRecord::Base.update_attribute, this method <b>is</b> # subject to normal validation routines as an update sends the whole body - # of the resource in the request. (See Validations). + # of the resource in the request. (See Validations). # # As such, this method is equivalent to calling update_attributes with a single attribute/value pair. # # If the saving fails because of a connection or remote service error, an - # exception will be raised. If saving fails because the resource is + # exception will be raised. If saving fails because the resource is # invalid then <tt>false</tt> will be returned. def update_attribute(name, value) self.send("#{name}=".to_sym, value) @@ -1305,7 +1322,7 @@ module ActiveResource # and requests that the record be saved. # # If the saving fails because of a connection or remote service error, an - # exception will be raised. If saving fails because the resource is + # exception will be raised. If saving fails because the resource is # invalid then <tt>false</tt> will be returned. # # Note: Though this request can be made with a partial set of the diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index 94839c8c25..46060b6f74 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -39,14 +39,14 @@ module ActiveResource # Set URI for remote service. def site=(site) - @site = site.is_a?(URI) ? site : URI.parser.parse(site) + @site = site.is_a?(URI) ? site : URI.parse(site) @user = URI.parser.unescape(@site.user) if @site.user @password = URI.parser.unescape(@site.password) if @site.password end # Set the proxy for remote service. def proxy=(proxy) - @proxy = proxy.is_a?(URI) ? proxy : URI.parser.parse(proxy) + @proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy) end # Sets the user for remote service. @@ -166,38 +166,28 @@ module ActiveResource end def configure_http(http) - http = apply_ssl_options(http) - - # Net::HTTP timeouts default to 60 seconds. - if @timeout - http.open_timeout = @timeout - http.read_timeout = @timeout + apply_ssl_options(http).tap do |https| + # Net::HTTP timeouts default to 60 seconds. + if defined? @timeout + https.open_timeout = @timeout + https.read_timeout = @timeout + end end - - http end def apply_ssl_options(http) - return http unless @site.is_a?(URI::HTTPS) - - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - return http unless defined?(@ssl_options) - - http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path] - http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file] + http.tap do |https| + # Skip config if site is already a https:// URI. + if defined? @ssl_options + http.use_ssl = true - http.cert = @ssl_options[:cert] if @ssl_options[:cert] - http.key = @ssl_options[:key] if @ssl_options[:key] + # Default to no cert verification (WTF? FIXME) + http.verify_mode = OpenSSL::SSL::VERIFY_NONE - http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store] - http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout] - - http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode] - http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback] - http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth] - - http + # All the SSL options have corresponding http settings. + @ssl_options.each { |key, value| http.send "#{key}=", value } + end + end end def default_header diff --git a/activeresource/lib/active_resource/custom_methods.rb b/activeresource/lib/active_resource/custom_methods.rb index 2a651dd48e..a0eb28ed13 100644 --- a/activeresource/lib/active_resource/custom_methods.rb +++ b/activeresource/lib/active_resource/custom_methods.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/object/blank' module ActiveResource # A module to support custom REST methods and sub-resources, allowing you to break out - # of the "default" REST methods with your own custom resource requests. For example, + # of the "default" REST methods with your own custom resource requests. For example, # say you use Rails to expose a REST service and configure your routes with: # # map.resources :people, :new => { :register => :post }, @@ -20,7 +20,7 @@ module ActiveResource # standard methods. # # class Person < ActiveResource::Base - # self.site = "http://37s.sunrise.i:3000" + # self.site = "https://37s.sunrise.com" # end # # Person.new(:name => 'Ryan').post(:register) # POST /people/new/register.json diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index 36f52d61d3..666b961f87 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -4,7 +4,7 @@ require 'active_support/core_ext/object/inclusion' module ActiveResource class InvalidRequestError < StandardError; end #:nodoc: - # One thing that has always been a pain with remote web services is testing. The HttpMock + # One thing that has always been a pain with remote web services is testing. The HttpMock # class makes it easy to test your Active Resource models by creating a set of mock responses to specific # requests. # @@ -15,17 +15,17 @@ module ActiveResource # # mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {}) # - # * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +put+, +delete+ or + # * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +put+, +delete+ or # +head+. # * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be # called. - # * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a - # hash format, such as <tt>{ "Content-Type" => "application/json" }</tt>. This mock will only trigger + # * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a + # hash format, such as <tt>{ "Content-Type" => "application/json" }</tt>. This mock will only trigger # if your tests sends a request with identical headers. - # * <tt>body</tt> - The data to be returned. This should be a string of Active Resource parseable content, + # * <tt>body</tt> - The data to be returned. This should be a string of Active Resource parseable content, # such as Json. # * <tt>status</tt> - The HTTP response code, as an integer, to return with the response. - # * <tt>response_headers</tt> - Headers to be returned with the response. Uses the same hash format as + # * <tt>response_headers</tt> - Headers to be returned with the response. Uses the same hash format as # <tt>request_headers</tt> listed above. # # In order for a mock to deliver its content, the incoming request must match by the <tt>http_method</tt>, @@ -291,12 +291,9 @@ module ActiveResource if resp_cls && !resp_cls.body_permitted? @body = nil end - - if @body.nil? - self['Content-Length'] = "0" - else - self['Content-Length'] = body.size.to_s - end + + self['Content-Length'] = @body.nil? ? "0" : body.size.to_s + end # Returns true if code is 2xx, diff --git a/activeresource/test/cases/authorization_test.rb b/activeresource/test/cases/authorization_test.rb index 0185e5432d..fbfe086599 100644 --- a/activeresource/test/cases/authorization_test.rb +++ b/activeresource/test/cases/authorization_test.rb @@ -9,8 +9,18 @@ class AuthorizationTest < ActiveSupport::TestCase @david = { :person => { :id => 2, :name => 'David' } }.to_json @authenticated_conn = ActiveResource::Connection.new("http://david:test123@localhost") @basic_authorization_request_header = { 'Authorization' => 'Basic ZGF2aWQ6dGVzdDEyMw==' } + end - @nonce = "MTI0OTUxMzc4NzpjYWI3NDM3NDNmY2JmODU4ZjQ2ZjcwNGZkMTJiMjE0NA==" + private + def decode(response) + @authenticated_conn.format.decode(response.body) + end +end + +class BasicAuthorizationTest < AuthorizationTest + def setup + super + @authenticated_conn.auth_type = :basic ActiveResource::HttpMock.respond_to do |mock| mock.get "/people/2.json", @basic_authorization_request_header, @david @@ -19,34 +29,48 @@ class AuthorizationTest < ActiveSupport::TestCase mock.delete "/people/2.json", @basic_authorization_request_header, nil, 200 mock.post "/people/2/addresses.json", @basic_authorization_request_header, nil, 201, 'Location' => '/people/1/addresses/5' mock.head "/people/2.json", @basic_authorization_request_header, nil, 200 + end + end - mock.get "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "fad396f6a34aeba28e28b9b96ddbb671") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } - mock.get "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "c064d5ba8891a25290c76c8c7d31fb7b") }, @david, 200 - mock.get "/people/1.json", { 'Authorization' => request_digest_auth_header("/people/1.json", "f9c0b594257bb8422af4abd429c5bb70") }, @matz, 200 + def test_get + david = decode(@authenticated_conn.get("/people/2.json")) + assert_equal "David", david["name"] + end - mock.put "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "50a685d814f94665b9d160fbbaa3958a") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } - mock.put "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "5a75cde841122d8e0f20f8fd1f98a743") }, nil, 204 + def test_post + response = @authenticated_conn.post("/people/2/addresses.json") + assert_equal "/people/1/addresses/5", response["Location"] + end - mock.delete "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "846f799107eab5ca4285b909ee299a33") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } - mock.delete "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "9f5b155224edbbb69fd99d8ce094681e") }, nil, 200 + def test_put + response = @authenticated_conn.put("/people/2.json") + assert_equal 204, response.code + end - mock.post "/people/2/addresses.json", { 'Authorization' => blank_digest_auth_header("/people/2/addresses.json", "6984d405ff3d9ed07bbf747dcf16afb0") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } - mock.post "/people/2/addresses.json", { 'Authorization' => request_digest_auth_header("/people/2/addresses.json", "4bda6a28dbf930b5af9244073623bd04") }, nil, 201, 'Location' => '/people/1/addresses/5' + def test_delete + response = @authenticated_conn.delete("/people/2.json") + assert_equal 200, response.code + end - mock.head "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "15e5ed84ba5c4cfcd5c98a36c2e4f421") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } - mock.head "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "d4c6d2bcc8717abb2e2ccb8c49ee6a91") }, nil, 200 - end + def test_head + response = @authenticated_conn.head("/people/2.json") + assert_equal 200, response.code + end - # Make client nonce deterministic - class << @authenticated_conn - private + def test_retry_on_401_doesnt_happen_with_basic_auth + assert_raise(ActiveResource::UnauthorizedAccess) { @authenticated_conn.get("/people/1.json") } + assert_equal "", @authenticated_conn.send(:response_auth_header) + end - def client_nonce - 'i-am-a-client-nonce' - end - end + def test_raises_invalid_request_on_unauthorized_requests + assert_raise(ActiveResource::InvalidRequestError) { @conn.get("/people/2.json") } + assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.json") } + assert_raise(ActiveResource::InvalidRequestError) { @conn.put("/people/2.json") } + assert_raise(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.json") } + assert_raise(ActiveResource::InvalidRequestError) { @conn.head("/people/2.json") } end + def test_authorization_header authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json')) assert_equal @basic_authorization_request_header['Authorization'], authorization_header['Authorization'] @@ -116,7 +140,6 @@ class AuthorizationTest < ActiveSupport::TestCase end def test_authorization_header_if_credentials_supplied_and_auth_type_is_basic - @authenticated_conn.auth_type = :basic authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json')) assert_equal @basic_authorization_request_header['Authorization'], authorization_header['Authorization'] authorization = authorization_header["Authorization"].to_s.split @@ -125,76 +148,77 @@ class AuthorizationTest < ActiveSupport::TestCase assert_equal ["david", "test123"], ::Base64.decode64(authorization[1]).split(":")[0..1] end - def test_authorization_header_if_credentials_supplied_and_auth_type_is_digest - @authenticated_conn.auth_type = :digest - authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json')) - assert_equal blank_digest_auth_header("/people/2.json", "fad396f6a34aeba28e28b9b96ddbb671"), authorization_header['Authorization'] + def test_client_nonce_is_not_nil + assert_not_nil ActiveResource::Connection.new("http://david:test123@localhost").send(:client_nonce) end +end - def test_authorization_header_with_query_string_if_auth_type_is_digest +class DigestAuthorizationTest < AuthorizationTest + def setup + super @authenticated_conn.auth_type = :digest - authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json?only=name')) - assert_equal blank_digest_auth_header("/people/2.json?only=name", "f8457b0b5d21b6b80737a386217afb24"), authorization_header['Authorization'] - end - def test_get - david = decode(@authenticated_conn.get("/people/2.json")) - assert_equal "David", david["name"] - end + # Make client nonce deterministic + def @authenticated_conn.client_nonce; 'i-am-a-client-nonce' end - def test_post - response = @authenticated_conn.post("/people/2/addresses.json") - assert_equal "/people/1/addresses/5", response["Location"] - end + @nonce = "MTI0OTUxMzc4NzpjYWI3NDM3NDNmY2JmODU4ZjQ2ZjcwNGZkMTJiMjE0NA==" - def test_put - response = @authenticated_conn.put("/people/2.json") - assert_equal 204, response.code + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "fad396f6a34aeba28e28b9b96ddbb671") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } + mock.get "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "c064d5ba8891a25290c76c8c7d31fb7b") }, @david, 200 + mock.get "/people/1.json", { 'Authorization' => request_digest_auth_header("/people/1.json", "f9c0b594257bb8422af4abd429c5bb70") }, @matz, 200 + + mock.put "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "50a685d814f94665b9d160fbbaa3958a") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } + mock.put "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "5a75cde841122d8e0f20f8fd1f98a743") }, nil, 204 + + mock.delete "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "846f799107eab5ca4285b909ee299a33") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } + mock.delete "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "9f5b155224edbbb69fd99d8ce094681e") }, nil, 200 + + mock.post "/people/2/addresses.json", { 'Authorization' => blank_digest_auth_header("/people/2/addresses.json", "6984d405ff3d9ed07bbf747dcf16afb0") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } + mock.post "/people/2/addresses.json", { 'Authorization' => request_digest_auth_header("/people/2/addresses.json", "4bda6a28dbf930b5af9244073623bd04") }, nil, 201, 'Location' => '/people/1/addresses/5' + + mock.head "/people/2.json", { 'Authorization' => blank_digest_auth_header("/people/2.json", "15e5ed84ba5c4cfcd5c98a36c2e4f421") }, nil, 401, { 'WWW-Authenticate' => response_digest_auth_header } + mock.head "/people/2.json", { 'Authorization' => request_digest_auth_header("/people/2.json", "d4c6d2bcc8717abb2e2ccb8c49ee6a91") }, nil, 200 + end end - def test_delete - response = @authenticated_conn.delete("/people/2.json") - assert_equal 200, response.code + def test_authorization_header_if_credentials_supplied_and_auth_type_is_digest + authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json')) + assert_equal blank_digest_auth_header("/people/2.json", "fad396f6a34aeba28e28b9b96ddbb671"), authorization_header['Authorization'] end - def test_head - response = @authenticated_conn.head("/people/2.json") - assert_equal 200, response.code + def test_authorization_header_with_query_string_if_auth_type_is_digest + authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json?only=name')) + assert_equal blank_digest_auth_header("/people/2.json?only=name", "f8457b0b5d21b6b80737a386217afb24"), authorization_header['Authorization'] end def test_get_with_digest_auth_handles_initial_401_response_and_retries - @authenticated_conn.auth_type = :digest response = @authenticated_conn.get("/people/2.json") assert_equal "David", decode(response)["name"] end def test_post_with_digest_auth_handles_initial_401_response_and_retries - @authenticated_conn.auth_type = :digest response = @authenticated_conn.post("/people/2/addresses.json") assert_equal "/people/1/addresses/5", response["Location"] assert_equal 201, response.code end def test_put_with_digest_auth_handles_initial_401_response_and_retries - @authenticated_conn.auth_type = :digest - response = @authenticated_conn.put("/people/2.json") - assert_equal 204, response.code + response = @authenticated_conn.put("/people/2.json") + assert_equal 204, response.code end def test_delete_with_digest_auth_handles_initial_401_response_and_retries - @authenticated_conn.auth_type = :digest response = @authenticated_conn.delete("/people/2.json") assert_equal 200, response.code end def test_head_with_digest_auth_handles_initial_401_response_and_retries - @authenticated_conn.auth_type = :digest response = @authenticated_conn.head("/people/2.json") assert_equal 200, response.code end def test_get_with_digest_auth_caches_nonce - @authenticated_conn.auth_type = :digest response = @authenticated_conn.get("/people/2.json") assert_equal "David", decode(response)["name"] @@ -203,19 +227,6 @@ class AuthorizationTest < ActiveSupport::TestCase assert_equal "Matz", decode(response)["name"] end - def test_retry_on_401_only_happens_with_digest_auth - assert_raise(ActiveResource::UnauthorizedAccess) { @authenticated_conn.get("/people/1.json") } - assert_equal "", @authenticated_conn.send(:response_auth_header) - end - - def test_raises_invalid_request_on_unauthorized_requests - assert_raise(ActiveResource::InvalidRequestError) { @conn.get("/people/2.json") } - assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.json") } - assert_raise(ActiveResource::InvalidRequestError) { @conn.put("/people/2.json") } - assert_raise(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.json") } - assert_raise(ActiveResource::InvalidRequestError) { @conn.head("/people/2.json") } - end - def test_raises_invalid_request_on_unauthorized_requests_with_digest_auth @conn.auth_type = :digest assert_raise(ActiveResource::InvalidRequestError) { @conn.get("/people/2.json") } @@ -225,17 +236,7 @@ class AuthorizationTest < ActiveSupport::TestCase assert_raise(ActiveResource::InvalidRequestError) { @conn.head("/people/2.json") } end - def test_client_nonce_is_not_nil - assert_not_nil ActiveResource::Connection.new("http://david:test123@localhost").send(:client_nonce) - end - - protected - def assert_response_raises(klass, code) - assert_raise(klass, "Expected response code #{code} to raise #{klass}") do - @conn.__send__(:handle_response, Response.new(code)) - end - end - + private def blank_digest_auth_header(uri, response) %Q(Digest username="david", realm="", qop="", uri="#{uri}", nonce="", nc="0", cnonce="i-am-a-client-nonce", opaque="", response="#{response}") end @@ -247,8 +248,4 @@ class AuthorizationTest < ActiveSupport::TestCase def response_digest_auth_header %Q(Digest realm="RailsTestApp", qop="auth", algorithm=MD5, nonce="#{@nonce}", opaque="ef6dfb078ba22298d366f99567814ffb") end - - def decode(response) - @authenticated_conn.format.decode(response.body) - end end diff --git a/activeresource/test/cases/connection_test.rb b/activeresource/test/cases/connection_test.rb index 653912f000..0a07ead15e 100644 --- a/activeresource/test/cases/connection_test.rb +++ b/activeresource/test/cases/connection_test.rb @@ -224,7 +224,6 @@ class ConnectionTest < ActiveSupport::TestCase http = Net::HTTP.new('') @conn.site="https://secure" @conn.ssl_options={:verify_mode => OpenSSL::SSL::VERIFY_PEER} - @conn.timeout = 10 # prevent warning about uninitialized. @conn.send(:configure_http, http) assert http.use_ssl? diff --git a/activeresource/test/fixtures/street_address.rb b/activeresource/test/fixtures/street_address.rb index 94a86702b0..6a8adb98b5 100644 --- a/activeresource/test/fixtures/street_address.rb +++ b/activeresource/test/fixtures/street_address.rb @@ -1,4 +1,4 @@ class StreetAddress < ActiveResource::Base - self.site = "http://37s.sunrise.i:3000/people/:person_id/" + self.site = "http://37s.sunrise.i:3000/people/:person_id" self.element_name = 'address' end |