activeresource-five 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,82 @@
1
+ module ActiveResource
2
+ class ConnectionError < StandardError # :nodoc:
3
+ attr_reader :response
4
+
5
+ def initialize(response, message = nil)
6
+ @response = response
7
+ @message = message
8
+ end
9
+
10
+ def to_s
11
+ message = "Failed."
12
+ message << " Response code = #{response.code}." if response.respond_to?(:code)
13
+ message << " Response message = #{response.message}." if response.respond_to?(:message)
14
+ message
15
+ end
16
+ end
17
+
18
+ # Raised when a Timeout::Error occurs.
19
+ class TimeoutError < ConnectionError
20
+ def initialize(message)
21
+ @message = message
22
+ end
23
+ def to_s; @message ;end
24
+ end
25
+
26
+ # Raised when a OpenSSL::SSL::SSLError occurs.
27
+ class SSLError < ConnectionError
28
+ def initialize(message)
29
+ @message = message
30
+ end
31
+ def to_s; @message ;end
32
+ end
33
+
34
+ # 3xx Redirection
35
+ class Redirection < ConnectionError # :nodoc:
36
+ def to_s
37
+ response['Location'] ? "#{super} => #{response['Location']}" : super
38
+ end
39
+ end
40
+
41
+ class MissingPrefixParam < ArgumentError # :nodoc:
42
+ end
43
+
44
+ # 4xx Client Error
45
+ class ClientError < ConnectionError # :nodoc:
46
+ end
47
+
48
+ # 400 Bad Request
49
+ class BadRequest < ClientError # :nodoc:
50
+ end
51
+
52
+ # 401 Unauthorized
53
+ class UnauthorizedAccess < ClientError # :nodoc:
54
+ end
55
+
56
+ # 403 Forbidden
57
+ class ForbiddenAccess < ClientError # :nodoc:
58
+ end
59
+
60
+ # 404 Not Found
61
+ class ResourceNotFound < ClientError # :nodoc:
62
+ end
63
+
64
+ # 409 Conflict
65
+ class ResourceConflict < ClientError # :nodoc:
66
+ end
67
+
68
+ # 410 Gone
69
+ class ResourceGone < ClientError # :nodoc:
70
+ end
71
+
72
+ # 5xx Server Error
73
+ class ServerError < ConnectionError # :nodoc:
74
+ end
75
+
76
+ # 405 Method Not Allowed
77
+ class MethodNotAllowed < ClientError # :nodoc:
78
+ def allowed_methods
79
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveResource
2
+ module Formats
3
+ autoload :XmlFormat, 'active_resource/formats/xml_format'
4
+ autoload :JsonFormat, 'active_resource/formats/json_format'
5
+
6
+ # Lookup the format class from a mime type reference symbol. Example:
7
+ #
8
+ # ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
9
+ # ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
10
+ def self.[](mime_type_reference)
11
+ ActiveResource::Formats.const_get(ActiveSupport::Inflector.camelize(mime_type_reference.to_s) + "Format")
12
+ end
13
+
14
+ def self.remove_root(data)
15
+ if data.is_a?(Hash) && data.keys.size == 1 && data.values.first.is_a?(Enumerable)
16
+ data.values.first
17
+ else
18
+ data
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/json'
2
+
3
+ module ActiveResource
4
+ module Formats
5
+ module JsonFormat
6
+ extend self
7
+
8
+ def extension
9
+ "json"
10
+ end
11
+
12
+ def mime_type
13
+ "application/json"
14
+ end
15
+
16
+ def encode(hash, options = nil)
17
+ ActiveSupport::JSON.encode(hash, options)
18
+ end
19
+
20
+ def decode(json)
21
+ Formats.remove_root(ActiveSupport::JSON.decode(json))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/core_ext/hash/conversions'
2
+
3
+ module ActiveResource
4
+ module Formats
5
+ module XmlFormat
6
+ extend self
7
+
8
+ def extension
9
+ "xml"
10
+ end
11
+
12
+ def mime_type
13
+ "application/xml"
14
+ end
15
+
16
+ def encode(hash, options={})
17
+ hash.to_xml(options)
18
+ end
19
+
20
+ def decode(xml)
21
+ Formats.remove_root(Hash.from_xml(xml))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,375 @@
1
+ require 'active_support/core_ext/kernel/reporting'
2
+ require 'active_support/core_ext/object/inclusion'
3
+
4
+ module ActiveResource
5
+ class InvalidRequestError < StandardError; end #:nodoc:
6
+
7
+ # One thing that has always been a pain with remote web services is testing. The HttpMock
8
+ # class makes it easy to test your Active Resource models by creating a set of mock responses to specific
9
+ # requests.
10
+ #
11
+ # To test your Active Resource model, you simply call the ActiveResource::HttpMock.respond_to
12
+ # method with an attached block. The block declares a set of URIs with expected input, and the output
13
+ # each request should return. The passed in block has any number of entries in the following generalized
14
+ # format:
15
+ #
16
+ # mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
17
+ #
18
+ # * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +patch+, +put+, +delete+ or
19
+ # +head+.
20
+ # * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be
21
+ # called.
22
+ # * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a
23
+ # hash format, such as <tt>{ "Content-Type" => "application/json" }</tt>. This mock will only trigger
24
+ # if your tests sends a request with identical headers.
25
+ # * <tt>body</tt> - The data to be returned. This should be a string of Active Resource parseable content,
26
+ # such as Json.
27
+ # * <tt>status</tt> - The HTTP response code, as an integer, to return with the response.
28
+ # * <tt>response_headers</tt> - Headers to be returned with the response. Uses the same hash format as
29
+ # <tt>request_headers</tt> listed above.
30
+ #
31
+ # In order for a mock to deliver its content, the incoming request must match by the <tt>http_method</tt>,
32
+ # +path+ and <tt>request_headers</tt>. If no match is found an +InvalidRequestError+ exception
33
+ # will be raised showing you what request it could not find a response for and also what requests and response
34
+ # pairs have been recorded so you can create a new mock for that request.
35
+ #
36
+ # ==== Example
37
+ # def setup
38
+ # @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
39
+ # ActiveResource::HttpMock.respond_to do |mock|
40
+ # mock.post "/people.json", {}, @matz, 201, "Location" => "/people/1.json"
41
+ # mock.get "/people/1.json", {}, @matz
42
+ # mock.put "/people/1.json", {}, nil, 204
43
+ # mock.delete "/people/1.json", {}, nil, 200
44
+ # end
45
+ # end
46
+ #
47
+ # def test_get_matz
48
+ # person = Person.find(1)
49
+ # assert_equal "Matz", person.name
50
+ # end
51
+ #
52
+ class HttpMock
53
+ class Responder #:nodoc:
54
+ def initialize(responses)
55
+ @responses = responses
56
+ end
57
+
58
+ [ :post, :patch, :put, :get, :delete, :head ].each do |method|
59
+ # def post(path, request_headers = {}, body = nil, status = 200, response_headers = {})
60
+ # @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
61
+ # end
62
+ module_eval <<-EOE, __FILE__, __LINE__ + 1
63
+ def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
64
+ request = Request.new(:#{method}, path, nil, request_headers)
65
+ response = Response.new(body || "", status, response_headers)
66
+
67
+ delete_duplicate_responses(request)
68
+
69
+ @responses << [request, response]
70
+ end
71
+ EOE
72
+ end
73
+
74
+ private
75
+
76
+ def delete_duplicate_responses(request)
77
+ @responses.delete_if {|r| r[0] == request }
78
+ end
79
+ end
80
+
81
+ class << self
82
+
83
+ # Returns an array of all request objects that have been sent to the mock. You can use this to check
84
+ # if your model actually sent an HTTP request.
85
+ #
86
+ # ==== Example
87
+ # def setup
88
+ # @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
89
+ # ActiveResource::HttpMock.respond_to do |mock|
90
+ # mock.get "/people/1.json", {}, @matz
91
+ # end
92
+ # end
93
+ #
94
+ # def test_should_request_remote_service
95
+ # person = Person.find(1) # Call the remote service
96
+ #
97
+ # # This request object has the same HTTP method and path as declared by the mock
98
+ # expected_request = ActiveResource::Request.new(:get, "/people/1.json")
99
+ #
100
+ # # Assert that the mock received, and responded to, the expected request from the model
101
+ # assert ActiveResource::HttpMock.requests.include?(expected_request)
102
+ # end
103
+ def requests
104
+ @@requests ||= []
105
+ end
106
+
107
+ # Returns the list of requests and their mocked responses. Look up a
108
+ # response for a request using <tt>responses.assoc(request)</tt>.
109
+ def responses
110
+ @@responses ||= []
111
+ end
112
+
113
+ # Accepts a block which declares a set of requests and responses for the HttpMock to respond to in
114
+ # the following format:
115
+ #
116
+ # mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
117
+ #
118
+ # === Example
119
+ #
120
+ # @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
121
+ # ActiveResource::HttpMock.respond_to do |mock|
122
+ # mock.post "/people.json", {}, @matz, 201, "Location" => "/people/1.json"
123
+ # mock.get "/people/1.json", {}, @matz
124
+ # mock.put "/people/1.json", {}, nil, 204
125
+ # mock.delete "/people/1.json", {}, nil, 200
126
+ # end
127
+ #
128
+ # Alternatively, accepts a hash of <tt>{Request => Response}</tt> pairs allowing you to generate
129
+ # these the following format:
130
+ #
131
+ # ActiveResource::Request.new(method, path, body, request_headers)
132
+ # ActiveResource::Response.new(body, status, response_headers)
133
+ #
134
+ # === Example
135
+ #
136
+ # Request.new(method, path, nil, request_headers)
137
+ #
138
+ # @matz = { :person => { :id => 1, :name => "Matz" } }.to_json
139
+ #
140
+ # create_matz = ActiveResource::Request.new(:post, '/people.json', @matz, {})
141
+ # created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.json"})
142
+ # get_matz = ActiveResource::Request.new(:get, '/people/1.json', nil)
143
+ # ok_response = ActiveResource::Response.new("", 200, {})
144
+ #
145
+ # pairs = {create_matz => created_response, get_matz => ok_response}
146
+ #
147
+ # ActiveResource::HttpMock.respond_to(pairs)
148
+ #
149
+ # Note, by default, every time you call +respond_to+, any previous request and response pairs stored
150
+ # in HttpMock will be deleted giving you a clean slate to work on.
151
+ #
152
+ # If you want to override this behavior, pass in +false+ as the last argument to +respond_to+
153
+ #
154
+ # === Example
155
+ #
156
+ # ActiveResource::HttpMock.respond_to do |mock|
157
+ # mock.send(:get, "/people/1", {}, "JSON1")
158
+ # end
159
+ # ActiveResource::HttpMock.responses.length #=> 1
160
+ #
161
+ # ActiveResource::HttpMock.respond_to(false) do |mock|
162
+ # mock.send(:get, "/people/2", {}, "JSON2")
163
+ # end
164
+ # ActiveResource::HttpMock.responses.length #=> 2
165
+ #
166
+ # This also works with passing in generated pairs of requests and responses, again, just pass in false
167
+ # as the last argument:
168
+ #
169
+ # === Example
170
+ #
171
+ # ActiveResource::HttpMock.respond_to do |mock|
172
+ # mock.send(:get, "/people/1", {}, "JSON1")
173
+ # end
174
+ # ActiveResource::HttpMock.responses.length #=> 1
175
+ #
176
+ # get_matz = ActiveResource::Request.new(:get, '/people/1.json', nil)
177
+ # ok_response = ActiveResource::Response.new("", 200, {})
178
+ #
179
+ # pairs = {get_matz => ok_response}
180
+ #
181
+ # ActiveResource::HttpMock.respond_to(pairs, false)
182
+ # ActiveResource::HttpMock.responses.length #=> 2
183
+ #
184
+ # # If you add a response with an existing request, it will be replaced
185
+ #
186
+ # fail_response = ActiveResource::Response.new("", 404, {})
187
+ # pairs = {get_matz => fail_response}
188
+ #
189
+ # ActiveResource::HttpMock.respond_to(pairs, false)
190
+ # ActiveResource::HttpMock.responses.length #=> 2
191
+ #
192
+ def respond_to(*args) #:yields: mock
193
+ pairs = args.first || {}
194
+ reset! if args.last.class != FalseClass
195
+
196
+ if block_given?
197
+ yield Responder.new(responses)
198
+ else
199
+ delete_responses_to_replace pairs.to_a
200
+ responses.concat pairs.to_a
201
+ Responder.new(responses)
202
+ end
203
+ end
204
+
205
+ def delete_responses_to_replace(new_responses)
206
+ new_responses.each{|nr|
207
+ request_to_remove = nr[0]
208
+ @@responses = responses.delete_if{|r| r[0] == request_to_remove}
209
+ }
210
+ end
211
+
212
+ # Deletes all logged requests and responses.
213
+ def reset!
214
+ requests.clear
215
+ responses.clear
216
+ end
217
+
218
+ # Enables all ActiveResource::Connection instances to use real
219
+ # Net::HTTP instance instead of a mock.
220
+ def enable_net_connection!
221
+ @@net_connection_enabled = true
222
+ end
223
+
224
+ # Sets all ActiveResource::Connection to use HttpMock instances.
225
+ def disable_net_connection!
226
+ @@net_connection_enabled = false
227
+ end
228
+
229
+ # Checks if real requests can be used instead of the default mock used in tests.
230
+ def net_connection_enabled?
231
+ if defined?(@@net_connection_enabled)
232
+ @@net_connection_enabled
233
+ else
234
+ @@net_connection_enabled = false
235
+ end
236
+ end
237
+
238
+ def net_connection_disabled?
239
+ !net_connection_enabled?
240
+ end
241
+
242
+ end
243
+
244
+ # body? methods
245
+ { true => %w(post patch put),
246
+ false => %w(get delete head) }.each do |has_body, methods|
247
+ methods.each do |method|
248
+ # def post(path, body, headers)
249
+ # request = ActiveResource::Request.new(:post, path, body, headers)
250
+ # self.class.requests << request
251
+ # if response = self.class.responses.assoc(request)
252
+ # response[1]
253
+ # else
254
+ # raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}")
255
+ # end
256
+ # end
257
+ module_eval <<-EOE, __FILE__, __LINE__ + 1
258
+ def #{method}(path, #{'body, ' if has_body}headers)
259
+ request = ActiveResource::Request.new(:#{method}, path, #{has_body ? 'body, ' : 'nil, '}headers)
260
+ self.class.requests << request
261
+ if response = self.class.responses.assoc(request)
262
+ response[1]
263
+ else
264
+ raise InvalidRequestError.new("Could not find a response recorded for \#{request.to_s} - Responses recorded are: \#{inspect_responses}")
265
+ end
266
+ end
267
+ EOE
268
+ end
269
+ end
270
+
271
+ def initialize(site) #:nodoc:
272
+ @site = site
273
+ end
274
+
275
+ def inspect_responses #:nodoc:
276
+ self.class.responses.map { |r| r[0].to_s }.inspect
277
+ end
278
+ end
279
+
280
+ class Request
281
+ attr_accessor :path, :method, :body, :headers
282
+
283
+ def initialize(method, path, body = nil, headers = {})
284
+ @method, @path, @body, @headers = method, path, body, headers
285
+ end
286
+
287
+ def ==(req)
288
+ path == req.path && method == req.method && headers_match?(req)
289
+ end
290
+
291
+ def to_s
292
+ "<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
293
+ end
294
+
295
+ private
296
+
297
+ def headers_match?(req)
298
+ # Ignore format header on equality if it's not defined
299
+ format_header = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method]
300
+ if headers[format_header].present? || req.headers[format_header].blank?
301
+ headers == req.headers
302
+ else
303
+ headers.dup.merge(format_header => req.headers[format_header]) == req.headers
304
+ end
305
+ end
306
+ end
307
+
308
+ class Response
309
+ attr_accessor :body, :message, :code, :headers
310
+
311
+ def initialize(body, message = 200, headers = {})
312
+ @body, @message, @headers = body, message.to_s, headers
313
+ @code = @message[0,3].to_i
314
+
315
+ resp_cls = Net::HTTPResponse::CODE_TO_OBJ[@code.to_s]
316
+ if resp_cls && !resp_cls.body_permitted?
317
+ @body = nil
318
+ end
319
+
320
+ self['Content-Length'] = @body.nil? ? "0" : body.size.to_s
321
+
322
+ end
323
+
324
+ # Returns true if code is 2xx,
325
+ # false otherwise.
326
+ def success?
327
+ code.in?(200..299)
328
+ end
329
+
330
+ def [](key)
331
+ headers[key]
332
+ end
333
+
334
+ def []=(key, value)
335
+ headers[key] = value
336
+ end
337
+
338
+ # Returns true if the other is a Response with an equal body, equal message
339
+ # and equal headers. Otherwise it returns false.
340
+ def ==(other)
341
+ if (other.is_a?(Response))
342
+ other.body == body && other.message == message && other.headers == headers
343
+ else
344
+ false
345
+ end
346
+ end
347
+ end
348
+
349
+ class Connection
350
+ private
351
+ silence_warnings do
352
+ def http
353
+ if unstub_http?
354
+ @http = configure_http(new_http)
355
+ elsif stub_http?
356
+ @http = http_stub
357
+ end
358
+ @http ||= http_stub
359
+ end
360
+
361
+ def http_stub
362
+ HttpMock.new(@site)
363
+ end
364
+
365
+ def unstub_http?
366
+ HttpMock.net_connection_enabled? && defined?(@http) && @http.kind_of?(HttpMock)
367
+ end
368
+
369
+ def stub_http?
370
+ HttpMock.net_connection_disabled? && defined?(@http) && @http.kind_of?(Net::HTTP)
371
+ end
372
+
373
+ end
374
+ end
375
+ end