2007-12-21 08:48:59 +01:00
|
|
|
require 'active_resource/connection'
|
|
|
|
|
|
|
|
module ActiveResource
|
|
|
|
class InvalidRequestError < StandardError; end #:nodoc:
|
|
|
|
|
2008-06-02 08:35:38 +02:00
|
|
|
# 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.
|
|
|
|
#
|
|
|
|
# To test your Active Resource model, you simply call the ActiveResource::HttpMock.respond_to
|
|
|
|
# method with an attached block. The block declares a set of URIs with expected input, and the output
|
|
|
|
# each request should return. The passed in block has any number of entries in the following generalized
|
|
|
|
# format:
|
|
|
|
#
|
|
|
|
# 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
|
|
|
|
# +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/xml" }</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,
|
|
|
|
# such as XML.
|
|
|
|
# * <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>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>,
|
|
|
|
# +path+ and <tt>request_headers</tt>. If no match is found an InvalidRequestError exception
|
|
|
|
# will be raised letting you know you need to create a new mock for that request.
|
|
|
|
#
|
|
|
|
# ==== Example
|
|
|
|
# def setup
|
|
|
|
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
|
|
|
# ActiveResource::HttpMock.respond_to do |mock|
|
|
|
|
# mock.post "/people.xml", {}, @matz, 201, "Location" => "/people/1.xml"
|
|
|
|
# mock.get "/people/1.xml", {}, @matz
|
|
|
|
# mock.put "/people/1.xml", {}, nil, 204
|
|
|
|
# mock.delete "/people/1.xml", {}, nil, 200
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# def test_get_matz
|
|
|
|
# person = Person.find(1)
|
|
|
|
# assert_equal "Matz", person.name
|
|
|
|
# end
|
|
|
|
#
|
2007-12-21 08:48:59 +01:00
|
|
|
class HttpMock
|
2008-06-02 08:35:38 +02:00
|
|
|
class Responder #:nodoc:
|
2007-12-21 08:48:59 +01:00
|
|
|
def initialize(responses)
|
|
|
|
@responses = responses
|
|
|
|
end
|
|
|
|
|
2008-05-18 06:22:34 +02:00
|
|
|
for method in [ :post, :put, :get, :delete, :head ]
|
2009-02-04 21:26:08 +01:00
|
|
|
# def post(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
|
|
|
# @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
|
|
|
|
# end
|
2008-05-18 06:22:34 +02:00
|
|
|
module_eval <<-EOE, __FILE__, __LINE__
|
2007-12-21 08:48:59 +01:00
|
|
|
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
|
|
|
@responses[Request.new(:#{method}, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
|
|
|
|
end
|
|
|
|
EOE
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class << self
|
2008-06-02 08:35:38 +02:00
|
|
|
|
|
|
|
# Returns an array of all request objects that have been sent to the mock. You can use this to check
|
2008-10-27 07:47:01 +01:00
|
|
|
# if your model actually sent an HTTP request.
|
2008-06-02 08:35:38 +02:00
|
|
|
#
|
|
|
|
# ==== Example
|
|
|
|
# def setup
|
|
|
|
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
|
|
|
# ActiveResource::HttpMock.respond_to do |mock|
|
|
|
|
# mock.get "/people/1.xml", {}, @matz
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# def test_should_request_remote_service
|
|
|
|
# person = Person.find(1) # Call the remote service
|
|
|
|
#
|
|
|
|
# # This request object has the same HTTP method and path as declared by the mock
|
|
|
|
# expected_request = ActiveResource::Request.new(:get, "/people/1.xml")
|
|
|
|
#
|
|
|
|
# # Assert that the mock received, and responded to, the expected request from the model
|
|
|
|
# assert ActiveResource::HttpMock.requests.include?(expected_request)
|
|
|
|
# end
|
2007-12-21 08:48:59 +01:00
|
|
|
def requests
|
|
|
|
@@requests ||= []
|
|
|
|
end
|
|
|
|
|
2008-06-02 08:35:38 +02:00
|
|
|
# Returns a hash of <tt>request => response</tt> pairs for all all responses this mock has delivered, where +request+
|
|
|
|
# is an instance of ActiveResource::Request and the response is, naturally, an instance of
|
|
|
|
# ActiveResource::Response.
|
2007-12-21 08:48:59 +01:00
|
|
|
def responses
|
|
|
|
@@responses ||= {}
|
|
|
|
end
|
|
|
|
|
2008-06-02 08:35:38 +02:00
|
|
|
# Accepts a block which declares a set of requests and responses for the HttpMock to respond to. See the main
|
|
|
|
# ActiveResource::HttpMock description for a more detailed explanation.
|
|
|
|
def respond_to(pairs = {}) #:yields: mock
|
2007-12-21 08:48:59 +01:00
|
|
|
reset!
|
|
|
|
pairs.each do |(path, response)|
|
|
|
|
responses[path] = response
|
|
|
|
end
|
|
|
|
|
|
|
|
if block_given?
|
|
|
|
yield Responder.new(responses)
|
|
|
|
else
|
|
|
|
Responder.new(responses)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2008-06-02 08:35:38 +02:00
|
|
|
# Deletes all logged requests and responses.
|
2007-12-21 08:48:59 +01:00
|
|
|
def reset!
|
|
|
|
requests.clear
|
|
|
|
responses.clear
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
for method in [ :post, :put ]
|
2009-02-04 21:26:08 +01:00
|
|
|
# def post(path, body, headers)
|
|
|
|
# request = ActiveResource::Request.new(:post, path, body, headers)
|
|
|
|
# self.class.requests << request
|
|
|
|
# self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for #{request}"))
|
|
|
|
# end
|
2008-05-18 06:22:34 +02:00
|
|
|
module_eval <<-EOE, __FILE__, __LINE__
|
2007-12-21 08:48:59 +01:00
|
|
|
def #{method}(path, body, headers)
|
|
|
|
request = ActiveResource::Request.new(:#{method}, path, body, headers)
|
|
|
|
self.class.requests << request
|
2008-05-18 06:22:34 +02:00
|
|
|
self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for \#{request}"))
|
2007-12-21 08:48:59 +01:00
|
|
|
end
|
|
|
|
EOE
|
|
|
|
end
|
|
|
|
|
2008-05-18 06:22:34 +02:00
|
|
|
for method in [ :get, :delete, :head ]
|
|
|
|
module_eval <<-EOE, __FILE__, __LINE__
|
2007-12-21 08:48:59 +01:00
|
|
|
def #{method}(path, headers)
|
|
|
|
request = ActiveResource::Request.new(:#{method}, path, nil, headers)
|
|
|
|
self.class.requests << request
|
2008-05-18 06:22:34 +02:00
|
|
|
self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for \#{request}"))
|
2007-12-21 08:48:59 +01:00
|
|
|
end
|
|
|
|
EOE
|
|
|
|
end
|
|
|
|
|
2008-06-02 08:35:38 +02:00
|
|
|
def initialize(site) #:nodoc:
|
2007-12-21 08:48:59 +01:00
|
|
|
@site = site
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Request
|
|
|
|
attr_accessor :path, :method, :body, :headers
|
|
|
|
|
|
|
|
def initialize(method, path, body = nil, headers = {})
|
2008-09-07 07:54:05 +02:00
|
|
|
@method, @path, @body, @headers = method, path, body, headers.merge(ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method] => 'application/xml')
|
2007-12-21 08:48:59 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def ==(other_request)
|
|
|
|
other_request.hash == hash
|
|
|
|
end
|
|
|
|
|
|
|
|
def eql?(other_request)
|
|
|
|
self == other_request
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
|
|
|
"<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
|
|
|
|
end
|
|
|
|
|
|
|
|
def hash
|
|
|
|
"#{path}#{method}#{headers}".hash
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Response
|
|
|
|
attr_accessor :body, :message, :code, :headers
|
|
|
|
|
|
|
|
def initialize(body, message = 200, headers = {})
|
|
|
|
@body, @message, @headers = body, message.to_s, headers
|
|
|
|
@code = @message[0,3].to_i
|
|
|
|
|
|
|
|
resp_cls = Net::HTTPResponse::CODE_TO_OBJ[@code.to_s]
|
|
|
|
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
|
|
|
|
end
|
|
|
|
|
|
|
|
def success?
|
|
|
|
(200..299).include?(code)
|
|
|
|
end
|
|
|
|
|
|
|
|
def [](key)
|
|
|
|
headers[key]
|
|
|
|
end
|
|
|
|
|
|
|
|
def []=(key, value)
|
|
|
|
headers[key] = value
|
|
|
|
end
|
|
|
|
|
|
|
|
def ==(other)
|
|
|
|
if (other.is_a?(Response))
|
|
|
|
other.body == body && other.message == message && other.headers == headers
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Connection
|
|
|
|
private
|
|
|
|
silence_warnings do
|
|
|
|
def http
|
|
|
|
@http ||= HttpMock.new(@site)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|