diff options
author | Emilio Tagua <miloops@gmail.com> | 2009-08-10 18:07:33 -0300 |
---|---|---|
committer | Emilio Tagua <miloops@gmail.com> | 2009-08-10 18:07:33 -0300 |
commit | 0e2fbd80e2420329738b891240d44a056cea1de4 (patch) | |
tree | 5b16755670be58e168b5e86e2cdcb43ee5aa3918 /activeresource | |
parent | eb3ae44ccaff1dc63eb31bf86d8db07c88ddc413 (diff) | |
parent | 600a89f2082beadf4af9fe140a1a2ae56386cd49 (diff) | |
download | rails-0e2fbd80e2420329738b891240d44a056cea1de4.tar.gz rails-0e2fbd80e2420329738b891240d44a056cea1de4.tar.bz2 rails-0e2fbd80e2420329738b891240d44a056cea1de4.zip |
Merge commit 'rails/master'
Conflicts:
activerecord/lib/active_record/calculations.rb
activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
Diffstat (limited to 'activeresource')
-rw-r--r-- | activeresource/CHANGELOG | 11 | ||||
-rw-r--r-- | activeresource/lib/active_resource/base.rb | 80 | ||||
-rw-r--r-- | activeresource/lib/active_resource/connection.rb | 68 | ||||
-rw-r--r-- | activeresource/lib/active_resource/exceptions.rb | 11 | ||||
-rw-r--r-- | activeresource/lib/active_resource/validations.rb | 24 | ||||
-rw-r--r-- | activeresource/test/base/load_test.rb | 17 | ||||
-rw-r--r-- | activeresource/test/base_errors_test.rb | 77 | ||||
-rw-r--r-- | activeresource/test/base_test.rb | 147 | ||||
-rw-r--r-- | activeresource/test/connection_test.rb | 42 | ||||
-rw-r--r-- | activeresource/test/fixtures/proxy.rb | 4 |
10 files changed, 445 insertions, 36 deletions
diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 6572934893..113694e895 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,3 +1,14 @@ +*Edge* + +* Add support for errors in JSON format. #1956 [Fabien Jakimowicz] + +* Recognizes 410 as Resource Gone. #2316 [Jordan Brough, Jatinder Singh] + +* More thorough SSL support. #2370 [Roy Nicholson] + +* HTTP proxy support. #2133 [Marshall Huss, Sébastien Dabet] + + *2.3.2 [Final] (March 15, 2009)* * Nothing new, just included in 2.3.2 diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index bc82139dac..88de8b1c66 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -103,6 +103,8 @@ module ActiveResource # # Many REST APIs will require authentication, usually in the form of basic # HTTP authentication. Authentication can be specified by: + # + # === HTTP Basic Authentication # * putting the credentials in the URL for the +site+ variable. # # class Person < ActiveResource::Base @@ -123,6 +125,19 @@ module ActiveResource # 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. # + # === Certificate Authentication + # + # * End point uses an X509 certificate for authentication. <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} + # end + # + # # == Errors & Validation # # Error handling and validation is handled in much the same manner as you're used to seeing in @@ -149,6 +164,7 @@ module ActiveResource # * 404 - ActiveResource::ResourceNotFound # * 405 - ActiveResource::MethodNotAllowed # * 409 - ActiveResource::ResourceConflict + # * 410 - ActiveResource::ResourceGone # * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors) # * 401..499 - ActiveResource::ClientError # * 500..599 - ActiveResource::ServerError @@ -169,7 +185,7 @@ module ActiveResource # # 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 representation of the validation errors. The save operation will + # 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) @@ -178,10 +194,14 @@ module ActiveResource # # # When # # PUT http://api.people.com:3000/people/1.xml + # # or + # # PUT http://api.people.com:3000/people/1.json # # is requested with invalid values, the response is: # # # # Response (422): # # <errors type="array"><error>First cannot be empty</error></errors> + # # or + # # {"errors":["First cannot be empty"]} # # # # ryan.errors.invalid?(:first) # => true @@ -257,6 +277,22 @@ module ActiveResource end end + # Gets the \proxy variable if a proxy is required + def proxy + # Not using superclass_delegating_reader. See +site+ for explanation + if defined?(@proxy) + @proxy + elsif superclass != Object && superclass.proxy + superclass.proxy.dup.freeze + end + end + + # Sets the URI of the http proxy to the value in the +proxy+ argument. + def proxy=(proxy) + @connection = nil + @proxy = proxy.nil? ? nil : create_proxy_uri_from(proxy) + end + # Gets the \user for REST HTTP authentication. def user # Not using superclass_delegating_reader. See +site+ for explanation @@ -326,15 +362,42 @@ module ActiveResource end end + # Options that will get applied to an SSL connection. + # + # * <tt>:key</tt> - An OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + # * <tt>:cert</tt> - An OpenSSL::X509::Certificate object as client certificate + # * <tt>:ca_file</tt> - Path to a CA certification file in PEM format. The file can contrain several CA certificates. + # * <tt>:ca_path</tt> - Path of a CA certification directory containing certifications in PEM format. + # * <tt>:verify_mode</tt> - Flags for server the certification verification at begining of SSL/TLS session. (OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable) + # * <tt>:verify_callback</tt> - The verify callback for the server certification verification. + # * <tt>:verify_depth</tt> - The maximum depth for the certificate chain verification. + # * <tt>:cert_store</tt> - OpenSSL::X509::Store to verify peer certificate. + # * <tt>:ssl_timeout</tt> -The SSL timeout in seconds. + def ssl_options=(opts={}) + @connection = nil + @ssl_options = opts + end + + # Returns the SSL options hash. + def ssl_options + if defined?(@ssl_options) + @ssl_options + elsif superclass != Object && superclass.ssl_options + superclass.ssl_options + end + end + # An instance of ActiveResource::Connection that is the base \connection to the remote service. # The +refresh+ parameter toggles whether or not the \connection is refreshed at every request # or not (defaults to <tt>false</tt>). def connection(refresh = false) if defined?(@connection) || superclass == Object @connection = Connection.new(site, format) if refresh || @connection.nil? + @connection.proxy = proxy if proxy @connection.user = user if user @connection.password = password if password @connection.timeout = timeout if timeout + @connection.ssl_options = ssl_options if ssl_options @connection else superclass.connection @@ -568,7 +631,7 @@ module ActiveResource response.code.to_i == 200 end # id && !find_single(id, options).nil? - rescue ActiveResource::ResourceNotFound + rescue ActiveResource::ResourceNotFound, ActiveResource::ResourceGone false end @@ -622,6 +685,11 @@ module ActiveResource 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.parse(proxy) + end + # contains a set of the current prefix parameters. def prefix_parameters @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set @@ -956,7 +1024,13 @@ module ActiveResource case value when Array resource = find_or_create_resource_for_collection(key) - value.map { |attrs| attrs.is_a?(String) ? attrs.dup : resource.new(attrs) } + value.map do |attrs| + if attrs.is_a?(String) || attrs.is_a?(Numeric) + attrs.duplicable? ? attrs.dup : attrs + else + resource.new(attrs) + end + end when Hash resource = find_or_create_resource_for(key) resource.new(value) diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index 99d4b8f2ca..9d551f04e7 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -13,10 +13,11 @@ module ActiveResource HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept', :put => 'Content-Type', :post => 'Content-Type', - :delete => 'Accept' + :delete => 'Accept', + :head => 'Accept' } - attr_reader :site, :user, :password, :timeout + attr_reader :site, :user, :password, :timeout, :proxy, :ssl_options attr_accessor :format class << self @@ -41,6 +42,11 @@ module ActiveResource @password = URI.decode(@site.password) if @site.password end + # Set the proxy for remote service. + def proxy=(proxy) + @proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy) + end + # Sets the user for remote service. def user=(user) @user = user @@ -56,6 +62,11 @@ module ActiveResource @timeout = timeout end + # Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'. + def ssl_options=(opts={}) + @ssl_options = opts + end + # Executes a GET request. # Used to get (find) resources. def get(path, headers = {}) @@ -83,7 +94,7 @@ module ActiveResource # Executes a HEAD request. # Used to obtain meta-information about resources, such as whether they exist and their size (via response headers). def head(path, headers = {}) - request(:head, path, build_request_headers(headers)) + request(:head, path, build_request_headers(headers, :head)) end @@ -97,6 +108,8 @@ module ActiveResource handle_response(result) rescue Timeout::Error => e raise TimeoutError.new(e.message) + rescue OpenSSL::SSL::SSLError => e + raise SSLError.new(e.message) end # Handles response and error codes from the remote service. @@ -118,6 +131,8 @@ module ActiveResource raise(MethodNotAllowed.new(response)) when 409 raise(ResourceConflict.new(response)) + when 410 + raise(ResourceGone.new(response)) when 422 raise(ResourceInvalid.new(response)) when 401...500 @@ -132,10 +147,49 @@ module ActiveResource # Creates new Net::HTTP instance for communication with the # remote service and resources. def http - http = Net::HTTP.new(@site.host, @site.port) - http.use_ssl = @site.is_a?(URI::HTTPS) - http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl? - http.read_timeout = @timeout if @timeout # If timeout is not set, the default Net::HTTP timeout (60s) is used. + configure_http(new_http) + end + + def new_http + if @proxy + Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password) + else + Net::HTTP.new(@site.host, @site.port) + end + 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 + 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.cert = @ssl_options[:cert] if @ssl_options[:cert] + http.key = @ssl_options[:key] if @ssl_options[:key] + + 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 end diff --git a/activeresource/lib/active_resource/exceptions.rb b/activeresource/lib/active_resource/exceptions.rb index 5e4b1d4487..0631cdcf9f 100644 --- a/activeresource/lib/active_resource/exceptions.rb +++ b/activeresource/lib/active_resource/exceptions.rb @@ -20,6 +20,14 @@ module ActiveResource def to_s; @message ;end end + # Raised when a OpenSSL::SSL::SSLError occurs. + class SSLError < ConnectionError + def initialize(message) + @message = message + end + def to_s; @message ;end + end + # 3xx Redirection class Redirection < ConnectionError # :nodoc: def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end @@ -43,6 +51,9 @@ module ActiveResource # 409 Conflict class ResourceConflict < ClientError; end # :nodoc: + # 410 Gone + class ResourceGone < ClientError; end # :nodoc: + # 5xx Server Error class ServerError < ConnectionError; end # :nodoc: diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb index a2ba224998..4ff7be6a9e 100644 --- a/activeresource/lib/active_resource/validations.rb +++ b/activeresource/lib/active_resource/validations.rb @@ -7,11 +7,10 @@ module ActiveResource # Active Resource validation is reported to and from this object, which is used by Base#save # to determine whether the object in a valid state to be saved. See usage example in Validations. class Errors < ActiveModel::Errors - # Grabs errors from the XML response. - def from_xml(xml) + # Grabs errors from an array of messages (like ActiveRecord::Validations) + def from_array(messages) clear humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) } - messages = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue [] messages.each do |message| attr_message = humanized_attributes.keys.detect do |attr_name| if message[0, attr_name.size + 1] == "#{attr_name} " @@ -22,6 +21,18 @@ module ActiveResource self[:base] << message if attr_message.nil? end end + + # Grabs errors from the json response. + def from_json(json) + array = ActiveSupport::JSON.decode(json)['errors'] rescue [] + from_array array + end + + # Grabs errors from the XML response. + def from_xml(xml) + array = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue [] + from_array array + end end # Module to support validation and errors with Active Resource objects. The module overrides @@ -56,7 +67,12 @@ module ActiveResource save_without_validation true rescue ResourceInvalid => error - errors.from_xml(error.response.body) + case error.response['Content-Type'] + when 'application/xml' + errors.from_xml(error.response.body) + when 'application/json' + errors.from_json(error.response.body) + end false end diff --git a/activeresource/test/base/load_test.rb b/activeresource/test/base/load_test.rb index 035bd965c2..5f5a580445 100644 --- a/activeresource/test/base/load_test.rb +++ b/activeresource/test/base/load_test.rb @@ -51,7 +51,9 @@ class BaseLoadTest < Test::Unit::TestCase :id => 1, :state => { :id => 1, :name => 'Oregon', :notable_rivers => [ { :id => 1, :name => 'Willamette' }, - { :id => 2, :name => 'Columbia', :rafted_by => @matz }] }}} + { :id => 2, :name => 'Columbia', :rafted_by => @matz }], + :postal_codes => [97018,1234567890], + :places => ["Columbia City", "Unknown"]}}} @person = Person.new end @@ -127,6 +129,19 @@ class BaseLoadTest < Test::Unit::TestCase assert_kind_of Person::Street::State::NotableRiver, rivers.first assert_equal @deep[:street][:state][:notable_rivers].first[:id], rivers.first.id assert_equal @matz[:id], rivers.last.rafted_by.id + + postal_codes = state.postal_codes + assert_kind_of Array, postal_codes + assert_equal 2, postal_codes.size + assert_kind_of Fixnum, postal_codes.first + assert_equal @deep[:street][:state][:postal_codes].first, postal_codes.first + assert_kind_of Bignum, postal_codes.last + assert_equal @deep[:street][:state][:postal_codes].last, postal_codes.last + + places = state.places + assert_kind_of Array, places + assert_kind_of String, places.first + assert_equal @deep[:street][:state][:places].first, places.first end def test_nested_collections_within_the_same_namespace diff --git a/activeresource/test/base_errors_test.rb b/activeresource/test/base_errors_test.rb index 28813821df..eca00e9ca8 100644 --- a/activeresource/test/base_errors_test.rb +++ b/activeresource/test/base_errors_test.rb @@ -4,45 +4,80 @@ require "fixtures/person" class BaseErrorsTest < Test::Unit::TestCase def setup ActiveResource::HttpMock.respond_to do |mock| - mock.post "/people.xml", {}, "<?xml version=\"1.0\" encoding=\"UTF-8\"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>", 422 + mock.post "/people.xml", {}, %q(<?xml version="1.0" encoding="UTF-8"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>), 422, {'Content-Type' => 'application/xml'} + mock.post "/people.json", {}, %q({"errors":["Age can't be blank","Name can't be blank","Name must start with a letter","Person quota full for today."]}), 422, {'Content-Type' => 'application/json'} end - @person = Person.new(:name => '', :age => '') - assert_equal @person.save, false end def test_should_mark_as_invalid - assert !@person.valid? + [ :json, :xml ].each do |format| + invalid_user_using_format(format) do + assert !@person.valid? + end + end end def test_should_parse_xml_errors - assert_kind_of ActiveResource::Errors, @person.errors - assert_equal 4, @person.errors.size + [ :json, :xml ].each do |format| + invalid_user_using_format(format) do + assert_kind_of ActiveResource::Errors, @person.errors + assert_equal 4, @person.errors.size + end + end end def test_should_parse_errors_to_individual_attributes - assert @person.errors[:name].any? - assert_equal ["can't be blank"], @person.errors[:age] - assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name] - assert_equal ["Person quota full for today."], @person.errors[:base] + [ :json, :xml ].each do |format| + invalid_user_using_format(format) do + assert @person.errors[:name].any? + assert_equal ["can't be blank"], @person.errors[:age] + assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name] + assert_equal ["Person quota full for today."], @person.errors[:base] + end + end end def test_should_iterate_over_errors - errors = [] - @person.errors.each { |attribute, message| errors << [attribute.to_s, message] } - assert errors.include?(["name", "can't be blank"]) + [ :json, :xml ].each do |format| + invalid_user_using_format(format) do + errors = [] + @person.errors.each { |attribute, message| errors << [attribute, message] } + assert errors.include?([:name, "can't be blank"]) + end + end end def test_should_iterate_over_full_errors - errors = [] - @person.errors.to_a.each { |message| errors << message } - assert errors.include?("Name can't be blank") + [ :json, :xml ].each do |format| + invalid_user_using_format(format) do + errors = [] + @person.errors.to_a.each { |message| errors << message } + assert errors.include?("Name can't be blank") + end + end end def test_should_format_full_errors - full = @person.errors.full_messages - assert full.include?("Age can't be blank") - assert full.include?("Name can't be blank") - assert full.include?("Name must start with a letter") - assert full.include?("Person quota full for today.") + [ :json, :xml ].each do |format| + invalid_user_using_format(format) do + full = @person.errors.full_messages + assert full.include?("Age can't be blank") + assert full.include?("Name can't be blank") + assert full.include?("Name must start with a letter") + assert full.include?("Person quota full for today.") + end + end + end + + private + def invalid_user_using_format(mime_type_reference) + previous_format = Person.format + Person.format = mime_type_reference + @person = Person.new(:name => '', :age => '') + assert_equal false, @person.save + + yield + ensure + Person.format = previous_format end end diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb index 82d3b2ae96..9c236bc893 100644 --- a/activeresource/test/base_test.rb +++ b/activeresource/test/base_test.rb @@ -3,6 +3,7 @@ require "fixtures/person" require "fixtures/customer" require "fixtures/street_address" require "fixtures/beast" +require "fixtures/proxy" require 'active_support/core_ext/hash/conversions' class BaseTest < Test::Unit::TestCase @@ -125,6 +126,28 @@ class BaseTest < Test::Unit::TestCase assert_nil actor.site end + def test_proxy_accessor_accepts_uri_or_string_argument + proxy = URI.parse('http://localhost') + + assert_nothing_raised { Person.proxy = 'http://localhost' } + assert_equal proxy, Person.proxy + + assert_nothing_raised { Person.proxy = proxy } + assert_equal proxy, Person.proxy + end + + def test_should_use_proxy_prefix_and_credentials + assert_equal 'http://user:password@proxy.local:3000', ProxyResource.proxy.to_s + end + + def test_proxy_variable_can_be_reset + actor = Class.new(ActiveResource::Base) + assert_nil actor.site + actor.proxy = 'http://localhost:31337' + actor.proxy = nil + assert_nil actor.site + end + def test_should_accept_setting_user Forum.user = 'david' assert_equal('david', Forum.user) @@ -143,6 +166,13 @@ class BaseTest < Test::Unit::TestCase assert_equal(5, Forum.connection.timeout) end + def test_should_accept_setting_ssl_options + expected = {:verify => 1} + Forum.ssl_options= expected + assert_equal(expected, Forum.ssl_options) + assert_equal(expected, Forum.connection.ssl_options) + end + def test_user_variable_can_be_reset actor = Class.new(ActiveResource::Base) actor.site = 'http://cinema' @@ -173,6 +203,16 @@ class BaseTest < Test::Unit::TestCase assert_nil actor.connection.timeout end + def test_ssl_options_hash_can_be_reset + actor = Class.new(ActiveResource::Base) + actor.site = 'https://cinema' + assert_nil actor.ssl_options + actor.ssl_options = {:foo => 5} + actor.ssl_options = nil + assert_nil actor.ssl_options + assert_nil actor.connection.ssl_options + end + def test_credentials_from_site_are_decoded actor = Class.new(ActiveResource::Base) actor.site = 'http://my%40email.com:%31%32%33@cinema' @@ -221,6 +261,47 @@ class BaseTest < Test::Unit::TestCase assert_equal fruit.site, apple.site, 'subclass did not adopt changes from parent class' end + def test_proxy_reader_uses_superclass_site_until_written + # Superclass is Object so returns nil. + assert_nil ActiveResource::Base.proxy + assert_nil Class.new(ActiveResource::Base).proxy + + # Subclass uses superclass proxy. + actor = Class.new(Person) + assert_equal Person.proxy, actor.proxy + + # Subclass returns frozen superclass copy. + assert !Person.proxy.frozen? + assert actor.proxy.frozen? + + # Changing subclass proxy doesn't change superclass site. + actor.proxy = 'http://localhost:31337' + assert_not_equal Person.proxy, actor.proxy + + # Changed subclass proxy is not frozen. + assert !actor.proxy.frozen? + + # Changing superclass proxy doesn't overwrite subclass site. + Person.proxy = 'http://somewhere.else' + assert_not_equal Person.proxy, actor.proxy + + # Changing superclass proxy after subclassing changes subclass site. + jester = Class.new(actor) + actor.proxy = 'http://nomad' + assert_equal actor.proxy, jester.proxy + assert jester.proxy.frozen? + + # Subclasses are always equal to superclass proxy when not overridden + fruit = Class.new(ActiveResource::Base) + apple = Class.new(fruit) + + fruit.proxy = 'http://market' + assert_equal fruit.proxy, apple.proxy, 'subclass did not adopt changes from parent class' + + fruit.proxy = 'http://supermarket' + assert_equal fruit.proxy, apple.proxy, 'subclass did not adopt changes from parent class' + end + def test_user_reader_uses_superclass_user_until_written # Superclass is Object so returns nil. assert_nil ActiveResource::Base.user @@ -331,6 +412,40 @@ class BaseTest < Test::Unit::TestCase assert_equal fruit.timeout, apple.timeout, 'subclass did not adopt changes from parent class' end + def test_ssl_options_reader_uses_superclass_ssl_options_until_written + # Superclass is Object so returns nil. + assert_nil ActiveResource::Base.ssl_options + assert_nil Class.new(ActiveResource::Base).ssl_options + Person.ssl_options = {:foo => 'bar'} + + # Subclass uses superclass ssl_options. + actor = Class.new(Person) + assert_equal Person.ssl_options, actor.ssl_options + + # Changing subclass ssl_options doesn't change superclass ssl_options. + actor.ssl_options = {:baz => ''} + assert_not_equal Person.ssl_options, actor.ssl_options + + # Changing superclass ssl_options doesn't overwrite subclass ssl_options. + Person.ssl_options = {:color => 'blue'} + assert_not_equal Person.ssl_options, actor.ssl_options + + # Changing superclass ssl_options after subclassing changes subclass ssl_options. + jester = Class.new(actor) + actor.ssl_options = {:color => 'red'} + assert_equal actor.ssl_options, jester.ssl_options + + # Subclasses are always equal to superclass ssl_options when not overridden. + fruit = Class.new(ActiveResource::Base) + apple = Class.new(fruit) + + fruit.ssl_options = {:alpha => 'betas'} + assert_equal fruit.ssl_options, apple.ssl_options, 'subclass did not adopt changes from parent class' + + fruit.ssl_options = {:omega => 'moos'} + assert_equal fruit.ssl_options, apple.ssl_options, 'subclass did not adopt changes from parent class' + end + def test_updating_baseclass_site_object_wipes_descendent_cached_connection_objects # Subclasses are always equal to superclass site when not overridden fruit = Class.new(ActiveResource::Base) @@ -784,6 +899,14 @@ class BaseTest < Test::Unit::TestCase assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :params => { :person_id => 1 }) } end + def test_destroy_with_410_gone + assert Person.find(1).destroy + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/1.xml", {}, nil, 410 + end + assert_raise(ActiveResource::ResourceGone) { Person.find(1).destroy } + end + def test_delete assert Person.delete(1) ActiveResource::HttpMock.respond_to do |mock| @@ -799,6 +922,14 @@ class BaseTest < Test::Unit::TestCase end assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :params => { :person_id => 1 }) } end + + def test_delete_with_410_gone + assert Person.delete(1) + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/1.xml", {}, nil, 410 + end + assert_raise(ActiveResource::ResourceGone) { Person.find(1) } + end def test_exists # Class method. @@ -851,6 +982,22 @@ class BaseTest < Test::Unit::TestCase end end + def test_exists_without_http_mock + http = Net::HTTP.new(Person.site.host, Person.site.port) + ActiveResource::Connection.any_instance.expects(:http).returns(http) + http.expects(:request).returns(ActiveResource::Response.new("")) + + assert Person.exists?('not-mocked') + end + + def test_exists_with_410_gone + ActiveResource::HttpMock.respond_to do |mock| + mock.head "/people/1.xml", {}, nil, 410 + end + + assert !Person.exists?(1) + end + def test_to_xml matz = Person.find(1) xml = matz.encode diff --git a/activeresource/test/connection_test.rb b/activeresource/test/connection_test.rb index 831fbc4003..d7466c65b4 100644 --- a/activeresource/test/connection_test.rb +++ b/activeresource/test/connection_test.rb @@ -56,6 +56,9 @@ class ConnectionTest < Test::Unit::TestCase # 409 is an optimistic locking error assert_response_raises ActiveResource::ResourceConflict, 409 + # 410 is a removed resource + assert_response_raises ActiveResource::ResourceGone, 410 + # 422 is a validation error assert_response_raises ActiveResource::ResourceInvalid, 422 @@ -101,6 +104,16 @@ class ConnectionTest < Test::Unit::TestCase assert_equal site, @conn.site end + def test_proxy_accessor_accepts_uri_or_string_argument + proxy = URI.parse("http://proxy_user:proxy_password@proxy.local:4242") + + assert_nothing_raised { @conn.proxy = "http://proxy_user:proxy_password@proxy.local:4242" } + assert_equal proxy, @conn.proxy + + assert_nothing_raised { @conn.proxy = proxy } + assert_equal proxy, @conn.proxy + end + def test_timeout_accessor @conn.timeout = 5 assert_equal 5, @conn.timeout @@ -175,6 +188,17 @@ class ConnectionTest < Test::Unit::TestCase assert_raise(ActiveResource::TimeoutError) { @conn.get('/people_timeout.xml') } end + def test_setting_timeout + http = Net::HTTP.new('') + + [10, 20].each do |timeout| + @conn.timeout = timeout + @conn.send(:configure_http, http) + assert_equal timeout, http.open_timeout + assert_equal timeout, http.read_timeout + end + end + def test_accept_http_header @http = mock('new Net::HTTP') @conn.expects(:http).returns(@http) @@ -183,6 +207,24 @@ class ConnectionTest < Test::Unit::TestCase assert_nothing_raised(Mocha::ExpectationError) { @conn.get(path, {'Accept' => 'application/xhtml+xml'}) } end + def test_ssl_options_get_applied_to_http + 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? + assert_equal http.verify_mode, OpenSSL::SSL::VERIFY_PEER + end + + def test_ssl_error + http = Net::HTTP.new('') + @conn.expects(:http).returns(http) + http.expects(:get).raises(OpenSSL::SSL::SSLError, 'Expired certificate') + assert_raise(ActiveResource::SSLError) { @conn.get('/people/1.xml') } + end + protected def assert_response_raises(klass, code) assert_raise(klass, "Expected response code #{code} to raise #{klass}") do diff --git a/activeresource/test/fixtures/proxy.rb b/activeresource/test/fixtures/proxy.rb new file mode 100644 index 0000000000..bb8e015df0 --- /dev/null +++ b/activeresource/test/fixtures/proxy.rb @@ -0,0 +1,4 @@ +class ProxyResource < ActiveResource::Base + self.site = "http://localhost" + self.proxy = "http://user:password@proxy.local:3000" +end
\ No newline at end of file |