activeresource 2.0.5 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activeresource might be problematic. Click here for more details.

@@ -18,6 +18,14 @@ module ActiveResource
18
18
  end
19
19
  end
20
20
 
21
+ # Raised when a Timeout::Error occurs.
22
+ class TimeoutError < ConnectionError
23
+ def initialize(message)
24
+ @message = message
25
+ end
26
+ def to_s; @message ;end
27
+ end
28
+
21
29
  # 3xx Redirection
22
30
  class Redirection < ConnectionError # :nodoc:
23
31
  def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
@@ -55,7 +63,7 @@ module ActiveResource
55
63
  # This class is used by ActiveResource::Base to interface with REST
56
64
  # services.
57
65
  class Connection
58
- attr_reader :site
66
+ attr_reader :site, :user, :password, :timeout
59
67
  attr_accessor :format
60
68
 
61
69
  class << self
@@ -68,6 +76,7 @@ module ActiveResource
68
76
  # attribute to the URI for the remote resource service.
69
77
  def initialize(site, format = ActiveResource::Formats[:xml])
70
78
  raise ArgumentError, 'Missing site URI' unless site
79
+ @user = @password = nil
71
80
  self.site = site
72
81
  self.format = format
73
82
  end
@@ -75,6 +84,23 @@ module ActiveResource
75
84
  # Set URI for remote service.
76
85
  def site=(site)
77
86
  @site = site.is_a?(URI) ? site : URI.parse(site)
87
+ @user = URI.decode(@site.user) if @site.user
88
+ @password = URI.decode(@site.password) if @site.password
89
+ end
90
+
91
+ # Set user for remote service.
92
+ def user=(user)
93
+ @user = user
94
+ end
95
+
96
+ # Set password for remote service.
97
+ def password=(password)
98
+ @password = password
99
+ end
100
+
101
+ # Set the number of seconds after which HTTP requests to the remote service should time out.
102
+ def timeout=(timeout)
103
+ @timeout = timeout
78
104
  end
79
105
 
80
106
  # Execute a GET request.
@@ -101,6 +127,12 @@ module ActiveResource
101
127
  request(:post, path, body.to_s, build_request_headers(headers))
102
128
  end
103
129
 
130
+ # Execute a HEAD request.
131
+ # Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
132
+ def head(path, headers = {})
133
+ request(:head, path, build_request_headers(headers))
134
+ end
135
+
104
136
 
105
137
  private
106
138
  # Makes request to remote service.
@@ -108,8 +140,10 @@ module ActiveResource
108
140
  logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
109
141
  result = nil
110
142
  time = Benchmark.realtime { result = http.send(method, path, *arguments) }
111
- logger.info "--> #{result.code} #{result.message} (#{result.body ? result.body : 0}b %.2fs)" % time if logger
143
+ logger.info "--> #{result.code} #{result.message} (#{result.body ? result.body.length : 0}b %.2fs)" % time if logger
112
144
  handle_response(result)
145
+ rescue Timeout::Error => e
146
+ raise TimeoutError.new(e.message)
113
147
  end
114
148
 
115
149
  # Handles response and error codes from remote service.
@@ -148,21 +182,22 @@ module ActiveResource
148
182
  http = Net::HTTP.new(@site.host, @site.port)
149
183
  http.use_ssl = @site.is_a?(URI::HTTPS)
150
184
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
185
+ http.read_timeout = @timeout if @timeout # If timeout is not set, the default Net::HTTP timeout (60s) is used.
151
186
  http
152
187
  end
153
188
 
154
189
  def default_header
155
190
  @default_header ||= { 'Content-Type' => format.mime_type }
156
191
  end
157
-
192
+
158
193
  # Builds headers for request to remote service.
159
194
  def build_request_headers(headers)
160
195
  authorization_header.update(default_header).update(headers)
161
196
  end
162
-
163
- # Sets authorization header; authentication information is pulled from credentials provided with site URI.
197
+
198
+ # Sets authorization header
164
199
  def authorization_header
165
- (@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
200
+ (@user || @password ? { 'Authorization' => 'Basic ' + ["#{@user}:#{ @password}"].pack('m').delete("\r\n") } : {})
166
201
  end
167
202
 
168
203
  def logger #:nodoc:
@@ -7,12 +7,12 @@ module ActiveResource
7
7
  # :member => { :promote => :put, :deactivate => :delete }
8
8
  # :collection => { :active => :get }
9
9
  #
10
- # This route set creates routes for the following http requests:
10
+ # This route set creates routes for the following HTTP requests:
11
11
  #
12
- # POST /people/new/register.xml #=> PeopleController.register
13
- # PUT /people/1/promote.xml #=> PeopleController.promote with :id => 1
14
- # DELETE /people/1/deactivate.xml #=> PeopleController.deactivate with :id => 1
15
- # GET /people/active.xml #=> PeopleController.active
12
+ # POST /people/new/register.xml # PeopleController.register
13
+ # PUT /people/1/promote.xml # PeopleController.promote with :id => 1
14
+ # DELETE /people/1/deactivate.xml # PeopleController.deactivate with :id => 1
15
+ # GET /people/active.xml # PeopleController.active
16
16
  #
17
17
  # Using this module, Active Resource can use these custom REST methods just like the
18
18
  # standard methods.
@@ -48,8 +48,8 @@ module ActiveResource
48
48
  # # => [{:id => 1, :name => 'Ryan'}]
49
49
  #
50
50
  # Note: the objects returned from this method are not automatically converted
51
- # into ActiveResource instances - they are ordinary Hashes. If you are expecting
52
- # ActiveResource instances, use the <tt>find</tt> class method with the
51
+ # into Active Resource instances - they are ordinary Hashes. If you are expecting
52
+ # Active Resource instances, use the <tt>find</tt> class method with the
53
53
  # <tt>:from</tt> option. For example:
54
54
  #
55
55
  # Person.find(:all, :from => :active)
@@ -21,7 +21,7 @@ module ActiveResource
21
21
 
22
22
  private
23
23
  # Manipulate from_xml Hash, because xml_simple is not exactly what we
24
- # want for ActiveResource.
24
+ # want for Active Resource.
25
25
  def from_xml_data(data)
26
26
  if data.is_a?(Hash) && data.keys.size == 1
27
27
  data.values.first
@@ -3,13 +3,57 @@ require 'active_resource/connection'
3
3
  module ActiveResource
4
4
  class InvalidRequestError < StandardError; end #:nodoc:
5
5
 
6
+ # One thing that has always been a pain with remote web services is testing. The HttpMock
7
+ # class makes it easy to test your Active Resource models by creating a set of mock responses to specific
8
+ # requests.
9
+ #
10
+ # To test your Active Resource model, you simply call the ActiveResource::HttpMock.respond_to
11
+ # method with an attached block. The block declares a set of URIs with expected input, and the output
12
+ # each request should return. The passed in block has any number of entries in the following generalized
13
+ # format:
14
+ #
15
+ # mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
16
+ #
17
+ # * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +put+, +delete+ or
18
+ # +head+.
19
+ # * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be
20
+ # called.
21
+ # * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a
22
+ # hash format, such as <tt>{ "Content-Type" => "application/xml" }</tt>. This mock will only trigger
23
+ # if your tests sends a request with identical headers.
24
+ # * <tt>body</tt> - The data to be returned. This should be a string of Active Resource parseable content,
25
+ # such as XML.
26
+ # * <tt>status</tt> - The HTTP response code, as an integer, to return with the response.
27
+ # * <tt>response_headers</tt> - Headers to be returned with the response. Uses the same hash format as
28
+ # <tt>request_headers</tt> listed above.
29
+ #
30
+ # In order for a mock to deliver its content, the incoming request must match by the <tt>http_method</tt>,
31
+ # +path+ and <tt>request_headers</tt>. If no match is found an InvalidRequestError exception
32
+ # will be raised letting you know you need to create a new mock for that request.
33
+ #
34
+ # ==== Example
35
+ # def setup
36
+ # @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
37
+ # ActiveResource::HttpMock.respond_to do |mock|
38
+ # mock.post "/people.xml", {}, @matz, 201, "Location" => "/people/1.xml"
39
+ # mock.get "/people/1.xml", {}, @matz
40
+ # mock.put "/people/1.xml", {}, nil, 204
41
+ # mock.delete "/people/1.xml", {}, nil, 200
42
+ # end
43
+ # end
44
+ #
45
+ # def test_get_matz
46
+ # person = Person.find(1)
47
+ # assert_equal "Matz", person.name
48
+ # end
49
+ #
6
50
  class HttpMock
7
- class Responder
51
+ class Responder #:nodoc:
8
52
  def initialize(responses)
9
53
  @responses = responses
10
54
  end
11
55
 
12
- for method in [ :post, :put, :get, :delete ]
56
+ for method in [ :post, :put, :get, :delete, :head ]
13
57
  module_eval <<-EOE, __FILE__, __LINE__
14
58
  def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
15
59
  @responses[Request.new(:#{method}, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
@@ -19,15 +63,41 @@ module ActiveResource
19
63
  end
20
64
 
21
65
  class << self
66
+
67
+ # Returns an array of all request objects that have been sent to the mock. You can use this to check
68
+ # wether or not your model actually sent an HTTP request.
69
+ #
70
+ # ==== Example
71
+ # def setup
72
+ # @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
73
+ # ActiveResource::HttpMock.respond_to do |mock|
74
+ # mock.get "/people/1.xml", {}, @matz
75
+ # end
76
+ # end
77
+ #
78
+ # def test_should_request_remote_service
79
+ # person = Person.find(1) # Call the remote service
80
+ #
81
+ # # This request object has the same HTTP method and path as declared by the mock
82
+ # expected_request = ActiveResource::Request.new(:get, "/people/1.xml")
83
+ #
84
+ # # Assert that the mock received, and responded to, the expected request from the model
85
+ # assert ActiveResource::HttpMock.requests.include?(expected_request)
86
+ # end
22
87
  def requests
23
88
  @@requests ||= []
24
89
  end
25
90
 
91
+ # Returns a hash of <tt>request => response</tt> pairs for all all responses this mock has delivered, where +request+
92
+ # is an instance of ActiveResource::Request and the response is, naturally, an instance of
93
+ # ActiveResource::Response.
26
94
  def responses
27
95
  @@responses ||= {}
28
96
  end
29
97
 
30
- def respond_to(pairs = {})
98
+ # Accepts a block which declares a set of requests and responses for the HttpMock to respond to. See the main
99
+ # ActiveResource::HttpMock description for a more detailed explanation.
100
+ def respond_to(pairs = {}) #:yields: mock
31
101
  reset!
32
102
  pairs.each do |(path, response)|
33
103
  responses[path] = response
@@ -40,6 +110,7 @@ module ActiveResource
40
110
  end
41
111
  end
42
112
 
113
+ # Deletes all logged requests and responses.
43
114
  def reset!
44
115
  requests.clear
45
116
  responses.clear
@@ -56,7 +127,7 @@ module ActiveResource
56
127
  EOE
57
128
  end
58
129
 
59
- for method in [ :get, :delete ]
130
+ for method in [ :get, :delete, :head ]
60
131
  module_eval <<-EOE, __FILE__, __LINE__
61
132
  def #{method}(path, headers)
62
133
  request = ActiveResource::Request.new(:#{method}, path, nil, headers)
@@ -66,7 +137,7 @@ module ActiveResource
66
137
  EOE
67
138
  end
68
139
 
69
- def initialize(site)
140
+ def initialize(site) #:nodoc:
70
141
  @site = site
71
142
  end
72
143
  end
@@ -216,8 +216,8 @@ module ActiveResource
216
216
  end
217
217
  end
218
218
 
219
- # Module to allow validation of ActiveResource objects, which creates an Errors instance for every resource.
220
- # Methods are implemented by overriding +Base#validate+ or its variants Each of these methods can inspect
219
+ # Module to allow validation of Active Resource objects, which creates an Errors instance for every resource.
220
+ # Methods are implemented by overriding Base#validate or its variants Each of these methods can inspect
221
221
  # the state of the object, which usually means ensuring that a number of attributes have a certain value
222
222
  # (such as not empty, within a given range, matching a certain regular expression and so on).
223
223
  #
@@ -1,8 +1,8 @@
1
1
  module ActiveResource
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 2
4
- MINOR = 0
5
- TINY = 5
4
+ MINOR = 1
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -7,4 +7,16 @@ require 'active_resource/http_mock'
7
7
  $:.unshift "#{File.dirname(__FILE__)}/../test"
8
8
  require 'setter_trap'
9
9
 
10
- ActiveResource::Base.logger = Logger.new("#{File.dirname(__FILE__)}/debug.log")
10
+ ActiveResource::Base.logger = Logger.new("#{File.dirname(__FILE__)}/debug.log")
11
+
12
+ # Wrap tests that use Mocha and skip if unavailable.
13
+ def uses_mocha(test_name)
14
+ unless Object.const_defined?(:Mocha)
15
+ require 'mocha'
16
+ require 'stubba'
17
+ end
18
+ yield
19
+ rescue LoadError => load_error
20
+ raise unless load_error.message =~ /mocha/i
21
+ $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again."
22
+ end
@@ -1,4 +1,4 @@
1
- require "#{File.dirname(__FILE__)}/abstract_unit"
1
+ require 'abstract_unit'
2
2
 
3
3
  class AuthorizationTest < Test::Unit::TestCase
4
4
  Response = Struct.new(:code)
@@ -45,6 +45,47 @@ class AuthorizationTest < Test::Unit::TestCase
45
45
  assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
46
46
  end
47
47
 
48
+ def test_authorization_header_with_decoded_credentials_from_url
49
+ @conn = ActiveResource::Connection.new("http://my%40email.com:%31%32%33@localhost")
50
+ authorization_header = @conn.send!(:authorization_header)
51
+ authorization = authorization_header["Authorization"].to_s.split
52
+
53
+ assert_equal "Basic", authorization[0]
54
+ assert_equal ["my@email.com", "123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
55
+ end
56
+
57
+ def test_authorization_header_explicitly_setting_username_and_password
58
+ @authenticated_conn = ActiveResource::Connection.new("http://@localhost")
59
+ @authenticated_conn.user = 'david'
60
+ @authenticated_conn.password = 'test123'
61
+ authorization_header = @authenticated_conn.send!(:authorization_header)
62
+ assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
63
+ authorization = authorization_header["Authorization"].to_s.split
64
+
65
+ assert_equal "Basic", authorization[0]
66
+ assert_equal ["david", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
67
+ end
68
+
69
+ def test_authorization_header_explicitly_setting_username_but_no_password
70
+ @conn = ActiveResource::Connection.new("http://@localhost")
71
+ @conn.user = "david"
72
+ authorization_header = @conn.send!(:authorization_header)
73
+ authorization = authorization_header["Authorization"].to_s.split
74
+
75
+ assert_equal "Basic", authorization[0]
76
+ assert_equal ["david"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
77
+ end
78
+
79
+ def test_authorization_header_explicitly_setting_password_but_no_username
80
+ @conn = ActiveResource::Connection.new("http://@localhost")
81
+ @conn.password = "test123"
82
+ authorization_header = @conn.send!(:authorization_header)
83
+ authorization = authorization_header["Authorization"].to_s.split
84
+
85
+ assert_equal "Basic", authorization[0]
86
+ assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
87
+ end
88
+
48
89
  def test_get
49
90
  david = @authenticated_conn.get("/people/2.xml")
50
91
  assert_equal "David", david["name"]
@@ -1,6 +1,6 @@
1
- require "#{File.dirname(__FILE__)}/../abstract_unit"
2
- require "#{File.dirname(__FILE__)}/../fixtures/person"
3
- require "#{File.dirname(__FILE__)}/../fixtures/street_address"
1
+ require 'abstract_unit'
2
+ require 'fixtures/person'
3
+ require 'fixtures/street_address'
4
4
 
5
5
  class CustomMethodsTest < Test::Unit::TestCase
6
6
  def setup
@@ -32,6 +32,9 @@ class CustomMethodsTest < Test::Unit::TestCase
32
32
  mock.put "/people/1/addresses/sort.xml?by=name", {}, nil, 204
33
33
  mock.post "/people/1/addresses/new/link.xml", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml'
34
34
  end
35
+
36
+ Person.user = nil
37
+ Person.password = nil
35
38
  end
36
39
 
37
40
  def teardown
@@ -1,4 +1,4 @@
1
- require "#{File.dirname(__FILE__)}/../abstract_unit"
1
+ require 'abstract_unit'
2
2
  require "fixtures/person"
3
3
  require "fixtures/street_address"
4
4
 
@@ -1,4 +1,4 @@
1
- require "#{File.dirname(__FILE__)}/../abstract_unit"
1
+ require 'abstract_unit'
2
2
  require "fixtures/person"
3
3
  require "fixtures/street_address"
4
4
 
@@ -1,4 +1,4 @@
1
- require "#{File.dirname(__FILE__)}/abstract_unit"
1
+ require 'abstract_unit'
2
2
  require "fixtures/person"
3
3
 
4
4
  class BaseErrorsTest < Test::Unit::TestCase
data/test/base_test.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "#{File.dirname(__FILE__)}/abstract_unit"
1
+ require 'abstract_unit'
2
2
  require "fixtures/person"
3
3
  require "fixtures/street_address"
4
4
  require "fixtures/beast"
@@ -7,36 +7,49 @@ class BaseTest < Test::Unit::TestCase
7
7
  def setup
8
8
  @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
9
9
  @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
10
+ @greg = { :id => 3, :name => 'Greg' }.to_xml(:root => 'person')
10
11
  @addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
11
12
  @default_request_headers = { 'Content-Type' => 'application/xml' }
12
13
  @rick = { :name => "Rick", :age => 25 }.to_xml(:root => "person")
13
14
  @people = [{ :id => 1, :name => 'Matz' }, { :id => 2, :name => 'David' }].to_xml(:root => 'people')
14
15
  @people_david = [{ :id => 2, :name => 'David' }].to_xml(:root => 'people')
15
16
  @addresses = [{ :id => 1, :street => '12345 Street' }].to_xml(:root => 'addresses')
16
-
17
+
17
18
  ActiveResource::HttpMock.respond_to do |mock|
18
- mock.get "/people/1.xml", {}, @matz
19
- mock.get "/people/2.xml", {}, @david
20
- mock.get "/people/3.xml", {'key' => 'value'}, nil, 404
21
- mock.put "/people/1.xml", {}, nil, 204
22
- mock.delete "/people/1.xml", {}, nil, 200
23
- mock.delete "/people/2.xml", {}, nil, 400
24
- mock.get "/people/99.xml", {}, nil, 404
25
- mock.post "/people.xml", {}, @rick, 201, 'Location' => '/people/5.xml'
26
- mock.get "/people.xml", {}, @people
27
- mock.get "/people/1/addresses.xml", {}, @addresses
28
- mock.get "/people/1/addresses/1.xml", {}, @addy
29
- mock.get "/people/1/addresses/2.xml", {}, nil, 404
30
- mock.get "/people/2/addresses/1.xml", {}, nil, 404
31
- mock.put "/people/1/addresses/1.xml", {}, nil, 204
32
- mock.delete "/people/1/addresses/1.xml", {}, nil, 200
33
- mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5'
34
- mock.get "/people//addresses.xml", {}, nil, 404
35
- mock.get "/people//addresses/1.xml", {}, nil, 404
36
- mock.put "/people//addresses/1.xml", {}, nil, 404
37
- mock.delete "/people//addresses/1.xml", {}, nil, 404
38
- mock.post "/people//addresses.xml", {}, nil, 404
19
+ mock.get "/people/1.xml", {}, @matz
20
+ mock.get "/people/2.xml", {}, @david
21
+ mock.get "/people/Greg.xml", {}, @greg
22
+ mock.get "/people/4.xml", {'key' => 'value'}, nil, 404
23
+ mock.put "/people/1.xml", {}, nil, 204
24
+ mock.delete "/people/1.xml", {}, nil, 200
25
+ mock.delete "/people/2.xml", {}, nil, 400
26
+ mock.get "/people/99.xml", {}, nil, 404
27
+ mock.post "/people.xml", {}, @rick, 201, 'Location' => '/people/5.xml'
28
+ mock.get "/people.xml", {}, @people
29
+ mock.get "/people/1/addresses.xml", {}, @addresses
30
+ mock.get "/people/1/addresses/1.xml", {}, @addy
31
+ mock.get "/people/1/addresses/2.xml", {}, nil, 404
32
+ mock.get "/people/2/addresses/1.xml", {}, nil, 404
33
+ mock.get "/people/Greg/addresses/1.xml", {}, @addy
34
+ mock.put "/people/1/addresses/1.xml", {}, nil, 204
35
+ mock.delete "/people/1/addresses/1.xml", {}, nil, 200
36
+ mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5'
37
+ mock.get "/people//addresses.xml", {}, nil, 404
38
+ mock.get "/people//addresses/1.xml", {}, nil, 404
39
+ mock.put "/people//addresses/1.xml", {}, nil, 404
40
+ mock.delete "/people//addresses/1.xml", {}, nil, 404
41
+ mock.post "/people//addresses.xml", {}, nil, 404
42
+ mock.head "/people/1.xml", {}, nil, 200
43
+ mock.head "/people/Greg.xml", {}, nil, 200
44
+ mock.head "/people/99.xml", {}, nil, 404
45
+ mock.head "/people/1/addresses/1.xml", {}, nil, 200
46
+ mock.head "/people/1/addresses/2.xml", {}, nil, 404
47
+ mock.head "/people/2/addresses/1.xml", {}, nil, 404
48
+ mock.head "/people/Greg/addresses/1.xml", {}, nil, 200
39
49
  end
50
+
51
+ Person.user = nil
52
+ Person.password = nil
40
53
  end
41
54
 
42
55
 
@@ -63,6 +76,61 @@ class BaseTest < Test::Unit::TestCase
63
76
  assert_nil actor.site
64
77
  end
65
78
 
79
+ def test_should_accept_setting_user
80
+ Forum.user = 'david'
81
+ assert_equal('david', Forum.user)
82
+ assert_equal('david', Forum.connection.user)
83
+ end
84
+
85
+ def test_should_accept_setting_password
86
+ Forum.password = 'test123'
87
+ assert_equal('test123', Forum.password)
88
+ assert_equal('test123', Forum.connection.password)
89
+ end
90
+
91
+ def test_should_accept_setting_timeout
92
+ Forum.timeout = 5
93
+ assert_equal(5, Forum.timeout)
94
+ assert_equal(5, Forum.connection.timeout)
95
+ end
96
+
97
+ def test_user_variable_can_be_reset
98
+ actor = Class.new(ActiveResource::Base)
99
+ actor.site = 'http://cinema'
100
+ assert_nil actor.user
101
+ actor.user = 'username'
102
+ actor.user = nil
103
+ assert_nil actor.user
104
+ assert_nil actor.connection.user
105
+ end
106
+
107
+ def test_password_variable_can_be_reset
108
+ actor = Class.new(ActiveResource::Base)
109
+ actor.site = 'http://cinema'
110
+ assert_nil actor.password
111
+ actor.password = 'username'
112
+ actor.password = nil
113
+ assert_nil actor.password
114
+ assert_nil actor.connection.password
115
+ end
116
+
117
+ def test_timeout_variable_can_be_reset
118
+ actor = Class.new(ActiveResource::Base)
119
+ actor.site = 'http://cinema'
120
+ assert_nil actor.timeout
121
+ actor.timeout = 5
122
+ actor.timeout = nil
123
+ assert_nil actor.timeout
124
+ assert_nil actor.connection.timeout
125
+ end
126
+
127
+ def test_credentials_from_site_are_decoded
128
+ actor = Class.new(ActiveResource::Base)
129
+ actor.site = 'http://my%40email.com:%31%32%33@cinema'
130
+ assert_equal("my@email.com", actor.user)
131
+ assert_equal("123", actor.password)
132
+ end
133
+
66
134
  def test_site_reader_uses_superclass_site_until_written
67
135
  # Superclass is Object so returns nil.
68
136
  assert_nil ActiveResource::Base.site
@@ -98,12 +166,122 @@ class BaseTest < Test::Unit::TestCase
98
166
  apple = Class.new(fruit)
99
167
 
100
168
  fruit.site = 'http://market'
101
- assert_equal fruit.site, apple.site, 'subclass did not adopt changes to parent class'
169
+ assert_equal fruit.site, apple.site, 'subclass did not adopt changes from parent class'
102
170
 
103
171
  fruit.site = 'http://supermarket'
104
- assert_equal fruit.site, apple.site, 'subclass did not adopt changes to parent class'
172
+ assert_equal fruit.site, apple.site, 'subclass did not adopt changes from parent class'
105
173
  end
106
174
 
175
+ def test_user_reader_uses_superclass_user_until_written
176
+ # Superclass is Object so returns nil.
177
+ assert_nil ActiveResource::Base.user
178
+ assert_nil Class.new(ActiveResource::Base).user
179
+ Person.user = 'anonymous'
180
+
181
+ # Subclass uses superclass user.
182
+ actor = Class.new(Person)
183
+ assert_equal Person.user, actor.user
184
+
185
+ # Subclass returns frozen superclass copy.
186
+ assert !Person.user.frozen?
187
+ assert actor.user.frozen?
188
+
189
+ # Changing subclass user doesn't change superclass user.
190
+ actor.user = 'david'
191
+ assert_not_equal Person.user, actor.user
192
+
193
+ # Changing superclass user doesn't overwrite subclass user.
194
+ Person.user = 'john'
195
+ assert_not_equal Person.user, actor.user
196
+
197
+ # Changing superclass user after subclassing changes subclass user.
198
+ jester = Class.new(actor)
199
+ actor.user = 'john.doe'
200
+ assert_equal actor.user, jester.user
201
+
202
+ # Subclasses are always equal to superclass user when not overridden
203
+ fruit = Class.new(ActiveResource::Base)
204
+ apple = Class.new(fruit)
205
+
206
+ fruit.user = 'manager'
207
+ assert_equal fruit.user, apple.user, 'subclass did not adopt changes from parent class'
208
+
209
+ fruit.user = 'client'
210
+ assert_equal fruit.user, apple.user, 'subclass did not adopt changes from parent class'
211
+ end
212
+
213
+ def test_password_reader_uses_superclass_password_until_written
214
+ # Superclass is Object so returns nil.
215
+ assert_nil ActiveResource::Base.password
216
+ assert_nil Class.new(ActiveResource::Base).password
217
+ Person.password = 'my-password'
218
+
219
+ # Subclass uses superclass password.
220
+ actor = Class.new(Person)
221
+ assert_equal Person.password, actor.password
222
+
223
+ # Subclass returns frozen superclass copy.
224
+ assert !Person.password.frozen?
225
+ assert actor.password.frozen?
226
+
227
+ # Changing subclass password doesn't change superclass password.
228
+ actor.password = 'secret'
229
+ assert_not_equal Person.password, actor.password
230
+
231
+ # Changing superclass password doesn't overwrite subclass password.
232
+ Person.password = 'super-secret'
233
+ assert_not_equal Person.password, actor.password
234
+
235
+ # Changing superclass password after subclassing changes subclass password.
236
+ jester = Class.new(actor)
237
+ actor.password = 'even-more-secret'
238
+ assert_equal actor.password, jester.password
239
+
240
+ # Subclasses are always equal to superclass password when not overridden
241
+ fruit = Class.new(ActiveResource::Base)
242
+ apple = Class.new(fruit)
243
+
244
+ fruit.password = 'mega-secret'
245
+ assert_equal fruit.password, apple.password, 'subclass did not adopt changes from parent class'
246
+
247
+ fruit.password = 'ok-password'
248
+ assert_equal fruit.password, apple.password, 'subclass did not adopt changes from parent class'
249
+ end
250
+
251
+ def test_timeout_reader_uses_superclass_timeout_until_written
252
+ # Superclass is Object so returns nil.
253
+ assert_nil ActiveResource::Base.timeout
254
+ assert_nil Class.new(ActiveResource::Base).timeout
255
+ Person.timeout = 5
256
+
257
+ # Subclass uses superclass timeout.
258
+ actor = Class.new(Person)
259
+ assert_equal Person.timeout, actor.timeout
260
+
261
+ # Changing subclass timeout doesn't change superclass timeout.
262
+ actor.timeout = 10
263
+ assert_not_equal Person.timeout, actor.timeout
264
+
265
+ # Changing superclass timeout doesn't overwrite subclass timeout.
266
+ Person.timeout = 15
267
+ assert_not_equal Person.timeout, actor.timeout
268
+
269
+ # Changing superclass timeout after subclassing changes subclass timeout.
270
+ jester = Class.new(actor)
271
+ actor.timeout = 20
272
+ assert_equal actor.timeout, jester.timeout
273
+
274
+ # Subclasses are always equal to superclass timeout when not overridden.
275
+ fruit = Class.new(ActiveResource::Base)
276
+ apple = Class.new(fruit)
277
+
278
+ fruit.timeout = 25
279
+ assert_equal fruit.timeout, apple.timeout, 'subclass did not adopt changes from parent class'
280
+
281
+ fruit.timeout = 30
282
+ assert_equal fruit.timeout, apple.timeout, 'subclass did not adopt changes from parent class'
283
+ end
284
+
107
285
  def test_updating_baseclass_site_object_wipes_descendent_cached_connection_objects
108
286
  # Subclasses are always equal to superclass site when not overridden
109
287
  fruit = Class.new(ActiveResource::Base)
@@ -111,9 +289,60 @@ class BaseTest < Test::Unit::TestCase
111
289
 
112
290
  fruit.site = 'http://market'
113
291
  assert_equal fruit.connection.site, apple.connection.site
292
+ first_connection = apple.connection.object_id
114
293
 
115
294
  fruit.site = 'http://supermarket'
116
- assert_equal fruit.connection.site, apple.connection.site
295
+ assert_equal fruit.connection.site, apple.connection.site
296
+ second_connection = apple.connection.object_id
297
+ assert_not_equal(first_connection, second_connection, 'Connection should be re-created')
298
+ end
299
+
300
+ def test_updating_baseclass_user_wipes_descendent_cached_connection_objects
301
+ # Subclasses are always equal to superclass user when not overridden
302
+ fruit = Class.new(ActiveResource::Base)
303
+ apple = Class.new(fruit)
304
+ fruit.site = 'http://market'
305
+
306
+ fruit.user = 'david'
307
+ assert_equal fruit.connection.user, apple.connection.user
308
+ first_connection = apple.connection.object_id
309
+
310
+ fruit.user = 'john'
311
+ assert_equal fruit.connection.user, apple.connection.user
312
+ second_connection = apple.connection.object_id
313
+ assert_not_equal(first_connection, second_connection, 'Connection should be re-created')
314
+ end
315
+
316
+ def test_updating_baseclass_password_wipes_descendent_cached_connection_objects
317
+ # Subclasses are always equal to superclass password when not overridden
318
+ fruit = Class.new(ActiveResource::Base)
319
+ apple = Class.new(fruit)
320
+ fruit.site = 'http://market'
321
+
322
+ fruit.password = 'secret'
323
+ assert_equal fruit.connection.password, apple.connection.password
324
+ first_connection = apple.connection.object_id
325
+
326
+ fruit.password = 'supersecret'
327
+ assert_equal fruit.connection.password, apple.connection.password
328
+ second_connection = apple.connection.object_id
329
+ assert_not_equal(first_connection, second_connection, 'Connection should be re-created')
330
+ end
331
+
332
+ def test_updating_baseclass_timeout_wipes_descendent_cached_connection_objects
333
+ # Subclasses are always equal to superclass timeout when not overridden
334
+ fruit = Class.new(ActiveResource::Base)
335
+ apple = Class.new(fruit)
336
+ fruit.site = 'http://market'
337
+
338
+ fruit.timeout = 5
339
+ assert_equal fruit.connection.timeout, apple.connection.timeout
340
+ first_connection = apple.connection.object_id
341
+
342
+ fruit.timeout = 10
343
+ assert_equal fruit.connection.timeout, apple.connection.timeout
344
+ second_connection = apple.connection.object_id
345
+ assert_not_equal(first_connection, second_connection, 'Connection should be re-created')
117
346
  end
118
347
 
119
348
  def test_collection_name
@@ -144,6 +373,30 @@ class BaseTest < Test::Unit::TestCase
144
373
  def test_custom_element_path
145
374
  assert_equal '/people/1/addresses/1.xml', StreetAddress.element_path(1, :person_id => 1)
146
375
  assert_equal '/people/1/addresses/1.xml', StreetAddress.element_path(1, 'person_id' => 1)
376
+ assert_equal '/people/Greg/addresses/1.xml', StreetAddress.element_path(1, 'person_id' => 'Greg')
377
+ end
378
+
379
+ def test_custom_element_path_with_redefined_to_param
380
+ Person.module_eval do
381
+ alias_method :original_to_param_element_path, :to_param
382
+ def to_param
383
+ name
384
+ end
385
+ end
386
+
387
+ # Class method.
388
+ assert_equal '/people/Greg.xml', Person.element_path('Greg')
389
+
390
+ # Protected Instance method.
391
+ assert_equal '/people/Greg.xml', Person.find('Greg').send(:element_path)
392
+
393
+ ensure
394
+ # revert back to original
395
+ Person.module_eval do
396
+ # save the 'new' to_param so we don't get a warning about discarding the method
397
+ alias_method :element_path_to_param, :to_param
398
+ alias_method :to_param, :original_to_param_element_path
399
+ end
147
400
  end
148
401
 
149
402
  def test_custom_element_path_with_parameters
@@ -248,7 +501,7 @@ class BaseTest < Test::Unit::TestCase
248
501
 
249
502
  def test_custom_header
250
503
  Person.headers['key'] = 'value'
251
- assert_raises(ActiveResource::ResourceNotFound) { Person.find(3) }
504
+ assert_raises(ActiveResource::ResourceNotFound) { Person.find(4) }
252
505
  ensure
253
506
  Person.headers.delete('key')
254
507
  end
@@ -329,6 +582,26 @@ class BaseTest < Test::Unit::TestCase
329
582
  assert_equal address, address.reload
330
583
  end
331
584
 
585
+ def test_reload_with_redefined_to_param
586
+ Person.module_eval do
587
+ alias_method :original_to_param_reload, :to_param
588
+ def to_param
589
+ name
590
+ end
591
+ end
592
+
593
+ person = Person.find('Greg')
594
+ assert_equal person, person.reload
595
+
596
+ ensure
597
+ # revert back to original
598
+ Person.module_eval do
599
+ # save the 'new' to_param so we don't get a warning about discarding the method
600
+ alias_method :reload_to_param, :to_param
601
+ alias_method :to_param, :original_to_param_reload
602
+ end
603
+ end
604
+
332
605
  def test_reload_works_without_prefix_options
333
606
  person = Person.find(:first)
334
607
  assert_equal person, person.reload
@@ -351,6 +624,41 @@ class BaseTest < Test::Unit::TestCase
351
624
  assert_raises(ActiveResource::ResourceConflict) { Person.create(:name => 'Rick') }
352
625
  end
353
626
 
627
+ def test_clone
628
+ matz = Person.find(1)
629
+ matz_c = matz.clone
630
+ assert matz_c.new?
631
+ matz.attributes.each do |k, v|
632
+ assert_equal v, matz_c.send(k) if k != Person.primary_key
633
+ end
634
+ end
635
+
636
+ def test_nested_clone
637
+ addy = StreetAddress.find(1, :params => {:person_id => 1})
638
+ addy_c = addy.clone
639
+ assert addy_c.new?
640
+ addy.attributes.each do |k, v|
641
+ assert_equal v, addy_c.send(k) if k != StreetAddress.primary_key
642
+ end
643
+ assert_equal addy.prefix_options, addy_c.prefix_options
644
+ end
645
+
646
+ def test_complex_clone
647
+ matz = Person.find(1)
648
+ matz.address = StreetAddress.find(1, :params => {:person_id => matz.id})
649
+ matz.non_ar_hash = {:not => "an ARes instance"}
650
+ matz.non_ar_arr = ["not", "ARes"]
651
+ matz_c = matz.clone
652
+ assert matz_c.new?
653
+ assert_raises(NoMethodError) {matz_c.address}
654
+ assert_equal matz.non_ar_hash, matz_c.non_ar_hash
655
+ assert_equal matz.non_ar_arr, matz_c.non_ar_arr
656
+
657
+ # Test that actual copy, not just reference copy
658
+ matz.non_ar_hash[:not] = "changed"
659
+ assert_not_equal matz.non_ar_hash, matz_c.non_ar_hash
660
+ end
661
+
354
662
  def test_update
355
663
  matz = Person.find(:first)
356
664
  matz.name = "David"
@@ -437,6 +745,35 @@ class BaseTest < Test::Unit::TestCase
437
745
  assert !StreetAddress.new({:id => 2, :person_id => 1}).exists?
438
746
  end
439
747
 
748
+ def test_exists_with_redefined_to_param
749
+ Person.module_eval do
750
+ alias_method :original_to_param_exists, :to_param
751
+ def to_param
752
+ name
753
+ end
754
+ end
755
+
756
+ # Class method.
757
+ assert Person.exists?('Greg')
758
+
759
+ # Instance method.
760
+ assert Person.find('Greg').exists?
761
+
762
+ # Nested class method.
763
+ assert StreetAddress.exists?(1, :params => { :person_id => Person.find('Greg').to_param })
764
+
765
+ # Nested instance method.
766
+ assert StreetAddress.find(1, :params => { :person_id => Person.find('Greg').to_param }).exists?
767
+
768
+ ensure
769
+ # revert back to original
770
+ Person.module_eval do
771
+ # save the 'new' to_param so we don't get a warning about discarding the method
772
+ alias_method :exists_to_param, :to_param
773
+ alias_method :to_param, :original_to_param_exists
774
+ end
775
+ end
776
+
440
777
  def test_to_xml
441
778
  matz = Person.find(1)
442
779
  xml = matz.to_xml