Updated README.rdoc again
[feedcatcher.git] / vendor / rails / activeresource / lib / active_resource / http_mock.rb
1 require 'active_resource/connection'
2
3 module ActiveResource
4 class InvalidRequestError < StandardError; end #:nodoc:
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 #
50 class HttpMock
51 class Responder #:nodoc:
52 def initialize(responses)
53 @responses = responses
54 end
55
56 for method in [ :post, :put, :get, :delete, :head ]
57 # def post(path, request_headers = {}, body = nil, status = 200, response_headers = {})
58 # @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
59 # end
60 module_eval <<-EOE, __FILE__, __LINE__
61 def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
62 @responses << [Request.new(:#{method}, path, nil, request_headers), Response.new(body || "", status, response_headers)]
63 end
64 EOE
65 end
66 end
67
68 class << self
69
70 # Returns an array of all request objects that have been sent to the mock. You can use this to check
71 # if your model actually sent an HTTP request.
72 #
73 # ==== Example
74 # def setup
75 # @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
76 # ActiveResource::HttpMock.respond_to do |mock|
77 # mock.get "/people/1.xml", {}, @matz
78 # end
79 # end
80 #
81 # def test_should_request_remote_service
82 # person = Person.find(1) # Call the remote service
83 #
84 # # This request object has the same HTTP method and path as declared by the mock
85 # expected_request = ActiveResource::Request.new(:get, "/people/1.xml")
86 #
87 # # Assert that the mock received, and responded to, the expected request from the model
88 # assert ActiveResource::HttpMock.requests.include?(expected_request)
89 # end
90 def requests
91 @@requests ||= []
92 end
93
94 # Returns the list of requests and their mocked responses. Look up a
95 # response for a request using responses.assoc(request).
96 def responses
97 @@responses ||= []
98 end
99
100 # Accepts a block which declares a set of requests and responses for the HttpMock to respond to. See the main
101 # ActiveResource::HttpMock description for a more detailed explanation.
102 def respond_to(pairs = {}) #:yields: mock
103 reset!
104 responses.concat pairs.to_a
105 if block_given?
106 yield Responder.new(responses)
107 else
108 Responder.new(responses)
109 end
110 end
111
112 # Deletes all logged requests and responses.
113 def reset!
114 requests.clear
115 responses.clear
116 end
117 end
118
119 # body? methods
120 { true => %w(post put),
121 false => %w(get delete head) }.each do |has_body, methods|
122 methods.each do |method|
123 # def post(path, body, headers)
124 # request = ActiveResource::Request.new(:post, path, body, headers)
125 # self.class.requests << request
126 # self.class.responses.assoc(request).try(:second) || raise(InvalidRequestError.new("No response recorded for #{request}"))
127 # end
128 module_eval <<-EOE, __FILE__, __LINE__
129 def #{method}(path, #{'body, ' if has_body}headers)
130 request = ActiveResource::Request.new(:#{method}, path, #{has_body ? 'body, ' : 'nil, '}headers)
131 self.class.requests << request
132 self.class.responses.assoc(request).try(:second) || raise(InvalidRequestError.new("No response recorded for \#{request}"))
133 end
134 EOE
135 end
136 end
137
138 def initialize(site) #:nodoc:
139 @site = site
140 end
141 end
142
143 class Request
144 attr_accessor :path, :method, :body, :headers
145
146 def initialize(method, path, body = nil, headers = {})
147 @method, @path, @body, @headers = method, path, body, headers.merge(ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method] => 'application/xml')
148 end
149
150 def ==(req)
151 path == req.path && method == req.method && headers == req.headers
152 end
153
154 def to_s
155 "<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
156 end
157 end
158
159 class Response
160 attr_accessor :body, :message, :code, :headers
161
162 def initialize(body, message = 200, headers = {})
163 @body, @message, @headers = body, message.to_s, headers
164 @code = @message[0,3].to_i
165
166 resp_cls = Net::HTTPResponse::CODE_TO_OBJ[@code.to_s]
167 if resp_cls && !resp_cls.body_permitted?
168 @body = nil
169 end
170
171 if @body.nil?
172 self['Content-Length'] = "0"
173 else
174 self['Content-Length'] = body.size.to_s
175 end
176 end
177
178 def success?
179 (200..299).include?(code)
180 end
181
182 def [](key)
183 headers[key]
184 end
185
186 def []=(key, value)
187 headers[key] = value
188 end
189
190 def ==(other)
191 if (other.is_a?(Response))
192 other.body == body && other.message == message && other.headers == headers
193 else
194 false
195 end
196 end
197 end
198
199 class Connection
200 private
201 silence_warnings do
202 def http
203 @http ||= HttpMock.new(@site)
204 end
205 end
206 end
207 end