PageRenderTime 56ms CodeModel.GetById 20ms app.highlight 33ms RepoModel.GetById 0ms app.codeStats 0ms

/test/test_client.rb

https://github.com/earth2marsh/grackle
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