/test/test_client.rb
Ruby | 359 lines | 329 code | 22 blank | 8 comment | 7 complexity | 9b1edbfe0e7142d76f654aa5343b17fe MD5 | raw file
1require File.dirname(__FILE__) + '/test_helper'
2
3class TestClient < Test::Unit::TestCase
4
5 #Used for mocking HTTP requests
6 class Net::HTTP
7 class << self
8 attr_accessor :response, :request, :last_instance, :responder
9 end
10
11 def connect
12 # This needs to be overridden so SSL requests can be mocked
13 end
14
15 def request(req)
16 self.class.last_instance = self
17 if self.class.responder
18 self.class.responder.call(self,req)
19 else
20 self.class.request = req
21 self.class.response
22 end
23 end
24 end
25
26 class MockProxy < Net::HTTP
27 class << self
28 attr_accessor :started
29 [:response,:request,:last_instance,:responder].each do |m|
30 class_eval "
31 def #{m}; Net::HTTP.#{m}; end
32 def #{m}=(val); Net::HTTP.#{m} = val; end
33 "
34 end
35 end
36
37 def start
38 self.class.started = true
39 super
40 end
41 end
42
43 #Mock responses that conform to HTTPResponse's interface
44 class MockResponse < Net::HTTPResponse
45 #include Net::HTTPHeader
46 attr_accessor :code, :body
47 def initialize(code,body,headers={})
48 super
49 self.code = code
50 self.body = body
51 headers.each do |name, value|
52 self[name] = value
53 end
54 end
55 end
56
57 #Transport that collects info on requests and responses for testing purposes
58 class MockTransport < Grackle::Transport
59 attr_accessor :status, :body, :method, :url, :options, :timeout
60
61 def initialize(status,body,headers={})
62 Net::HTTP.response = MockResponse.new(status,body,headers)
63 end
64
65 def request(method, string_url, options)
66 self.method = method
67 self.url = URI.parse(string_url)
68 self.options = options
69 super(method,string_url,options)
70 end
71 end
72
73 class TestHandler
74 attr_accessor :decode_value
75
76 def initialize(value)
77 self.decode_value = value
78 end
79
80 def decode_response(body)
81 decode_value
82 end
83 end
84
85 def test_redirects
86 redirects = 2 #Check that we can follow 2 redirects before getting to original request
87 req_count = 0
88 responder = Proc.new do |inst, req|
89 req_count += 1
90 #Store the original request
91 if req_count == 1
92 inst.class.request = req
93 else
94 assert_equal("/somewhere_else#{req_count-1}.json",req.path)
95 end
96 if req_count <= redirects
97 MockResponse.new(302,"You are being redirected",'location'=>"http://twitter.com/somewhere_else#{req_count}.json")
98 else
99 inst.class.response
100 end
101 end
102 with_http_responder(responder) do
103 test_simple_get_request
104 end
105 assert_equal(redirects+1,req_count)
106 end
107
108 def test_timeouts
109 client = new_client(200,'{"id":12345,"screen_name":"test_user"}')
110 assert_equal(60, client.timeout)
111 client.timeout = 30
112 assert_equal(30, client.timeout)
113 end
114
115 def test_simple_get_request
116 client = new_client(200,'{"id":12345,"screen_name":"test_user"}')
117 value = client.users.show.json? :screen_name=>'test_user'
118 assert_equal(:get,client.transport.method)
119 assert_equal('http',client.transport.url.scheme)
120 assert(!Net::HTTP.last_instance.use_ssl?,'Net::HTTP instance should not be set to use SSL')
121 assert_equal('api.twitter.com',client.transport.url.host)
122 assert_equal('/1/users/show.json',client.transport.url.path)
123 assert_equal('test_user',client.transport.options[:params][:screen_name])
124 assert_equal('screen_name=test_user',Net::HTTP.request.path.split(/\?/)[1])
125 assert_equal(12345,value.id)
126 end
127
128 def test_simple_post_request_with_basic_auth
129 client = Grackle::Client.new(:auth=>{:type=>:basic,:username=>'fake_user',:password=>'fake_pass'})
130 test_simple_post(client) do
131 assert_match(/Basic/i,Net::HTTP.request['Authorization'],"Request should include Authorization header for basic auth")
132 end
133 end
134
135 def test_simple_post_request_with_oauth
136 client = Grackle::Client.new(:auth=>{:type=>:oauth,:consumer_key=>'12345',:consumer_secret=>'abc',:token=>'wxyz',:token_secret=>'98765'})
137 test_simple_post(client) do
138 auth = Net::HTTP.request['Authorization']
139 assert_match(/OAuth/i,auth,"Request should include Authorization header for OAuth")
140 assert_match(/oauth_consumer_key="12345"/,auth,"Auth header should include consumer key")
141 assert_match(/oauth_token="wxyz"/,auth,"Auth header should include token")
142 assert_match(/oauth_signature_method="HMAC-SHA1"/,auth,"Auth header should include HMAC-SHA1 signature method as that's what Twitter supports")
143 end
144 end
145
146 def test_ssl
147 client = new_client(200,'[{"id":1,"text":"test 1"}]',:ssl=>true)
148 client.statuses.public_timeline?
149 assert_equal("https",client.transport.url.scheme)
150 assert(Net::HTTP.last_instance.use_ssl?,'Net::HTTP instance should be set to use SSL')
151 end
152
153 def test_ssl_with_ca_cert_file
154 MockTransport.ca_cert_file = "some_ca_certs.pem"
155 client = new_client(200,'[{"id":1,"text":"test 1"}]',:ssl=>true)
156 client.statuses.public_timeline?
157 assert_equal(OpenSSL::SSL::VERIFY_PEER,Net::HTTP.last_instance.verify_mode,'Net::HTTP instance should use OpenSSL::SSL::VERIFY_PEER mode')
158 assert_equal(MockTransport.ca_cert_file,Net::HTTP.last_instance.ca_file,'Net::HTTP instance should have cert file set')
159 end
160
161 def test_default_format
162 client = new_client(200,'[{"id":1,"text":"test 1"}]',:default_format=>:json)
163 client.statuses.public_timeline?
164 assert_match(/\.json$/,client.transport.url.path)
165
166 client = new_client(200,'<statuses type="array"><status><id>1</id><text>test 1</text></status></statuses>',:default_format=>:xml)
167 client.statuses.public_timeline?
168 assert_match(/\.xml$/,client.transport.url.path)
169 end
170
171 def test_api
172 client = new_client(200,'[{"id":1,"text":"test 1"}]',:api=>:search)
173 client.search? :q=>'test'
174 assert_equal('search.twitter.com',client.transport.url.host)
175
176 client[:rest].users.show.some_user?
177 assert_equal('api.twitter.com',client.transport.url.host)
178
179 client.api = :search
180 client.trends?
181 assert_equal('search.twitter.com',client.transport.url.host)
182
183 client.api = :v1
184 client.search? :q=>'test'
185 assert_equal('api.twitter.com',client.transport.url.host)
186 assert_match(%r{^/1/search},client.transport.url.path)
187
188 client.api = :rest
189 client[:v1].users.show.some_user?
190 assert_equal('api.twitter.com',client.transport.url.host)
191 assert_match(%r{^/1/users/show/some_user},client.transport.url.path)
192 end
193
194 def test_headers
195 client = new_client(200,'[{"id":1,"text":"test 1"}]',:headers=>{'User-Agent'=>'TestAgent/1.0','X-Test-Header'=>'Header Value'})
196 client.statuses.public_timeline?
197 assert_equal('TestAgent/1.0',Net::HTTP.request['User-Agent'],"Custom User-Agent header should have been set")
198 assert_equal('Header Value',Net::HTTP.request['X-Test-Header'],"Custom X-Test-Header header should have been set")
199 end
200
201 def test_custom_handlers
202 client = new_client(200,'[{"id":1,"text":"test 1"}]',:handlers=>{:json=>TestHandler.new(42)})
203 value = client.statuses.public_timeline.json?
204 assert_equal(42,value)
205 end
206
207 def test_clear
208 client = new_client(200,'[{"id":1,"text":"test 1"}]')
209 client.some.url.that.does.not.exist
210 assert_equal('/some/url/that/does/not/exist',client.send(:request).path,"An unexecuted path should be built up")
211 client.clear
212 assert_equal('',client.send(:request).path,"The path shoudl be cleared")
213 end
214
215 def test_file_param_triggers_multipart_encoding
216 client = new_client(200,'[{"id":1,"text":"test 1"}]')
217 client.account.update_profile_image! :image=>File.new(__FILE__)
218 assert_match(/multipart\/form-data/,Net::HTTP.request['Content-Type'])
219 end
220
221 def test_time_param_is_http_encoded_and_escaped
222 client = new_client(200,'[{"id":1,"text":"test 1"}]')
223 time = Time.now-60*60
224 client.statuses.public_timeline? :since=>time
225 assert_equal("/1/statuses/public_timeline.json?since=#{CGI::escape(time.httpdate)}",Net::HTTP.request.path)
226 end
227
228 def test_simple_http_method_block
229 client = new_client(200,'[{"id":1,"text":"test 1"}]')
230 client.delete { direct_messages.destroy :id=>1, :other=>'value' }
231 assert_equal(:delete,client.transport.method, "delete block should use delete method")
232 assert_equal("/1/direct_messages/destroy/1.json",Net::HTTP.request.path)
233 assert_equal('value',client.transport.options[:params][:other])
234
235 client = new_client(200,'{"id":54321,"screen_name":"test_user"}')
236 value = client.get { users.show.json? :screen_name=>'test_user' }
237 assert_equal(:get,client.transport.method)
238 assert_equal('http',client.transport.url.scheme)
239 assert(!Net::HTTP.last_instance.use_ssl?,'Net::HTTP instance should not be set to use SSL')
240 assert_equal('api.twitter.com',client.transport.url.host)
241 assert_equal('/1/users/show.json',client.transport.url.path)
242 assert_equal('test_user',client.transport.options[:params][:screen_name])
243 assert_equal('screen_name=test_user',Net::HTTP.request.path.split(/\?/)[1])
244 assert_equal(54321,value.id)
245 end
246
247 def test_http_method_blocks_choose_right_method
248 client = new_client(200,'[{"id":1,"text":"test 1"}]')
249 client.get { search :q=>'test' }
250 assert_equal(:get,client.transport.method, "Get block should choose get method")
251 client.delete { direct_messages.destroy :id=>1 }
252 assert_equal(:delete,client.transport.method, "Delete block should choose delete method")
253 client.post { direct_messages.destroy :id=>1 }
254 assert_equal(:post,client.transport.method, "Post block should choose post method")
255 client.put { direct_messages :id=>1 }
256 assert_equal(:put,client.transport.method, "Put block should choose put method")
257 end
258
259 def test_http_method_selection_precedence
260 client = new_client(200,'[{"id":1,"text":"test 1"}]')
261 client.get { search! :q=>'test' }
262 assert_equal(:get,client.transport.method, "Get block should override method even if post bang is used")
263 client.delete { search? :q=>'test', :__method=>:post }
264 assert_equal(:post,client.transport.method, ":__method=>:post should override block setting and method suffix")
265 end
266
267 def test_underscore_method_works_with_numbers
268 client = new_client(200,'{"id":12345,"screen_name":"test_user"}')
269 value = client.users.show._(12345).json?
270 assert_equal(:get,client.transport.method)
271 assert_equal('http',client.transport.url.scheme)
272 assert(!Net::HTTP.last_instance.use_ssl?,'Net::HTTP instance should not be set to use SSL')
273 assert_equal('api.twitter.com',client.transport.url.host)
274 assert_equal('/1/users/show/12345.json',client.transport.url.path)
275 assert_equal(12345,value.id)
276 end
277
278 def test_transport_proxy_setting_is_used
279 client = new_client(200,'{"id":12345,"screen_name":"test_user"}')
280 called = false
281 call_trans = nil
282 client.transport.proxy = Proc.new {|trans| call_trans = trans; called = true; MockProxy }
283 client.users.show._(12345).json?
284 assert(called,"Proxy proc should be called during request")
285 assert(MockProxy.started,"Proxy should have been called")
286 assert_equal(client.transport,call_trans,"Proxy should have been called with transport")
287 MockProxy.started = false
288 client.transport.proxy = MockProxy
289 client.users.show._(12345).json?
290 assert(MockProxy.started,"Proxy should have been called")
291 MockProxy.started = false
292 client.transport.proxy = nil
293 assert_equal(false,MockProxy.started,"Proxy should not have been called")
294 end
295
296 def test_auto_append_ids_is_honored
297 client = new_client(200,'{"id":12345,"screen_name":"test_user"}')
298 client.users.show.json? :id=>12345
299 assert_equal('/1/users/show/12345.json',client.transport.url.path,"Id should be appended by default")
300 client.auto_append_ids = false
301 client.users.show.json? :id=>12345
302 assert_equal('/1/users/show.json',client.transport.url.path,"Id should not be appended")
303 assert_equal(12345,client.transport.options[:params][:id], "Id should be treated as a parameter")
304 assert_equal("id=#{12345}",Net::HTTP.request.path.split(/\?/)[1],"id should be part of the query string")
305 end
306
307 def test_auto_append_ids_can_be_set_in_constructor
308 client = new_client(200,'{"id":12345,"screen_name":"test_user"}',:auto_append_ids=>false)
309 client.users.show.json? :id=>12345
310 assert_equal('/1/users/show.json',client.transport.url.path,"Id should not be appended")
311 assert_equal(12345,client.transport.options[:params][:id], "Id should be treated as a parameter")
312 assert_equal("id=#{12345}",Net::HTTP.request.path.split(/\?/)[1],"id should be part of the query string")
313 end
314
315 def test_default_api
316 client = Grackle::Client.new
317 assert_equal(:v1,client.api,":v1 should be default api")
318 end
319
320 # Methods like Twitter's DELETE list membership expect that the user id will
321 # be form encoded like a POST request in the body. Net::HTTP seems to think
322 # that DELETEs can't have body parameters so we have to work around that.
323 def test_delete_can_send_body_parameters
324 client = new_client(200,'{"id":12345,"name":"Test List","members":0}')
325 client.delete { some_user.some_list.members? :user_id=>12345 }
326 assert_equal(:delete,client.transport.method,"Expected delete request")
327 assert_equal('http',client.transport.url.scheme,"Expected scheme to be http")
328 assert_equal('api.twitter.com',client.transport.url.host,"Expected request to be against twitter.com")
329 assert_equal('/1/some_user/some_list/members.json',client.transport.url.path)
330 assert_match(/user_id=12345/,Net::HTTP.request.body,"Parameters should be form encoded")
331 end
332
333 private
334 def with_http_responder(responder)
335 Net::HTTP.responder = responder
336 yield
337 ensure
338 Net::HTTP.responder = nil
339 end
340
341 def new_client(response_status, response_body, client_opts={})
342 client = Grackle::Client.new(client_opts)
343 client.transport = MockTransport.new(response_status,response_body)
344 client
345 end
346
347 def test_simple_post(client)
348 client.transport = MockTransport.new(200,'{"id":12345,"text":"test status"}')
349 value = client.statuses.update! :status=>'test status'
350 assert_equal(:post,client.transport.method,"Expected post request")
351 assert_equal('http',client.transport.url.scheme,"Expected scheme to be http")
352 assert_equal('api.twitter.com',client.transport.url.host,"Expected request to be against twitter.com")
353 assert_equal('/1/statuses/update.json',client.transport.url.path)
354 assert_match(/status=test%20status/,Net::HTTP.request.body,"Parameters should be form encoded")
355 assert_equal(12345,value.id)
356 yield(client) if block_given?
357 end
358
359end