bloveless_grackle 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,90 @@
1
+ module Grackle
2
+
3
+ # This module contain handlers that know how to take a response body
4
+ # from Twitter and turn it into a TwitterStruct return value. Handlers are
5
+ # used by the Client to give back return values from API calls. A handler
6
+ # is intended to provide a +decode_response+ method which accepts the response body
7
+ # as a string.
8
+ module Handlers
9
+
10
+ # Decodes JSON Twitter API responses
11
+ class JSONHandler
12
+
13
+ def decode_response(res)
14
+ json_result = JSON.parse(res)
15
+ load_recursive(json_result)
16
+ end
17
+
18
+ private
19
+ def load_recursive(value)
20
+ if value.kind_of? Hash
21
+ build_struct(value)
22
+ elsif value.kind_of? Array
23
+ value.map{|v| load_recursive(v)}
24
+ else
25
+ value
26
+ end
27
+ end
28
+
29
+ def build_struct(hash)
30
+ struct = TwitterStruct.new
31
+ hash.each do |key,v|
32
+ struct.send("#{key}=",load_recursive(v))
33
+ end
34
+ struct
35
+ end
36
+
37
+ end
38
+
39
+ # Decodes XML Twitter API responses
40
+ class XMLHandler
41
+
42
+ #Known nodes returned by twitter that contain arrays
43
+ ARRAY_NODES = ['ids','statuses','users']
44
+
45
+ def decode_response(res)
46
+ xml = REXML::Document.new(res)
47
+ load_recursive(xml.root)
48
+ end
49
+
50
+ private
51
+ def load_recursive(node)
52
+ if array_node?(node)
53
+ node.elements.map {|e| load_recursive(e)}
54
+ elsif node.elements.size > 0
55
+ build_struct(node)
56
+ elsif node.elements.size == 0
57
+ value = node.text
58
+ fixnum?(value) ? value.to_i : value
59
+ end
60
+ end
61
+
62
+ def build_struct(node)
63
+ ts = TwitterStruct.new
64
+ node.elements.each do |e|
65
+ ts.send("#{e.name}=",load_recursive(e))
66
+ end
67
+ ts
68
+ end
69
+
70
+ # Most of the time Twitter specifies nodes that contain an array of
71
+ # sub-nodes with a type="array" attribute. There are some nodes that
72
+ # they dont' do that for, though, including the <ids> node returned
73
+ # by the social graph methods. This method tries to work in both situations.
74
+ def array_node?(node)
75
+ node.attributes['type'] == 'array' || ARRAY_NODES.include?(node.name)
76
+ end
77
+
78
+ def fixnum?(value)
79
+ value =~ /^\d+$/
80
+ end
81
+ end
82
+
83
+ # Just echoes back the response body. This is primarily used for unknown formats
84
+ class StringHandler
85
+ def decode_response(res)
86
+ res
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,248 @@
1
+ module Grackle
2
+
3
+ class Response #:nodoc:
4
+ attr_accessor :method, :request_uri, :status, :body
5
+
6
+ def initialize(method,request_uri,status,body)
7
+ self.method = method
8
+ self.request_uri = request_uri
9
+ self.status = status
10
+ self.body = body
11
+ end
12
+ end
13
+
14
+ class Transport
15
+
16
+ attr_accessor :debug, :proxy
17
+
18
+ CRLF = "\r\n"
19
+ DEFAULT_REDIRECT_LIMIT = 5
20
+
21
+ class << self
22
+ attr_accessor :ca_cert_file
23
+ end
24
+
25
+ def req_class(method)
26
+ Net::HTTP.const_get(method.to_s.capitalize)
27
+ end
28
+
29
+ # Options are one of
30
+ # - :params - a hash of parameters to be sent with the request. If a File is a parameter value, \
31
+ # a multipart request will be sent. If a Time is included, .httpdate will be called on it.
32
+ # - :headers - a hash of headers to send with the request
33
+ # - :auth - a hash of authentication parameters for either basic or oauth
34
+ # - :timeout - timeout for the http request in seconds
35
+ def request(method, string_url, options={})
36
+ params = stringify_params(options[:params])
37
+ if method == :get && params
38
+ string_url << query_string(params)
39
+ end
40
+ url = URI.parse(string_url)
41
+ begin
42
+ execute_request(method,url,options)
43
+ rescue Timeout::Error
44
+ raise "Timeout while #{method}ing #{url.to_s}"
45
+ end
46
+ end
47
+
48
+ def execute_request(method,url,options={})
49
+ conn = http_class.new(url.host, url.port)
50
+ conn.use_ssl = (url.scheme == 'https')
51
+ if conn.use_ssl?
52
+ configure_ssl(conn)
53
+ end
54
+ conn.start do |http|
55
+ req = req_class(method).new(url.request_uri)
56
+ http.read_timeout = options[:timeout]
57
+ add_headers(req,options[:headers])
58
+ if file_param?(options[:params])
59
+ add_multipart_data(req,options[:params])
60
+ else
61
+ add_form_data(req,options[:params])
62
+ end
63
+ if options.has_key? :auth
64
+ if options[:auth][:type] == :basic
65
+ add_basic_auth(req,options[:auth])
66
+ elsif options[:auth][:type] == :oauth
67
+ add_oauth(http,req,options[:auth])
68
+ end
69
+ end
70
+ dump_request(req) if debug
71
+ res = http.request(req)
72
+ dump_response(res) if debug
73
+ redirect_limit = options[:redirect_limit] || DEFAULT_REDIRECT_LIMIT
74
+ if res.code.to_s =~ /^3\d\d$/ && redirect_limit > 0 && res['location']
75
+ execute_request(method,URI.parse(res['location']),options.merge(:redirect_limit=>redirect_limit-1))
76
+ else
77
+ Response.new(method,url.to_s,res.code.to_i,res.body)
78
+ end
79
+ end
80
+ end
81
+
82
+ def query_string(params)
83
+ query = case params
84
+ when Hash then params.map{|key,value| url_encode_param(key,value) }.join("&")
85
+ else url_encode(params.to_s)
86
+ end
87
+ if !(query == nil || query.length == 0) && query[0,1] != '?'
88
+ query = "?#{query}"
89
+ end
90
+ query
91
+ end
92
+
93
+ private
94
+ def stringify_params(params)
95
+ return nil unless params
96
+ params.inject({}) do |h, pair|
97
+ key, value = pair
98
+ if value.respond_to? :httpdate
99
+ value = value.httpdate
100
+ end
101
+ h[key] = value
102
+ h
103
+ end
104
+ end
105
+
106
+ def file_param?(params)
107
+ return false unless params
108
+ params.any? {|key,value| value.respond_to? :read }
109
+ end
110
+
111
+ def url_encode(value)
112
+ require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
113
+ CGI.escape(value.to_s)
114
+ end
115
+
116
+ def url_encode_param(key,value)
117
+ "#{url_encode(key)}=#{url_encode(value)}"
118
+ end
119
+
120
+ def add_headers(req,headers)
121
+ if headers
122
+ headers.each do |header, value|
123
+ req[header] = value
124
+ end
125
+ end
126
+ end
127
+
128
+ def add_form_data(req,params)
129
+ if request_body_permitted?(req) && params
130
+ req.set_form_data(params)
131
+ end
132
+ end
133
+
134
+ def add_multipart_data(req,params)
135
+ boundary = Time.now.to_i.to_s(16)
136
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
137
+ body = ""
138
+ params.each do |key,value|
139
+ esc_key = url_encode(key)
140
+ body << "--#{boundary}#{CRLF}"
141
+ if value.respond_to?(:read)
142
+ mime_type = MIME::Types.type_for(value.path)[0] || MIME::Types["application/octet-stream"][0]
143
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{CRLF}"
144
+ body << "Content-Type: #{mime_type.simplified}#{CRLF*2}"
145
+ body << value.read
146
+ else
147
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{CRLF*2}#{value}"
148
+ end
149
+ body << CRLF
150
+ end
151
+ body << "--#{boundary}--#{CRLF*2}"
152
+ req.body = body
153
+ req["Content-Length"] = req.body.size
154
+ end
155
+
156
+ def add_basic_auth(req,auth)
157
+ username = auth[:username]
158
+ password = auth[:password]
159
+ if username && password
160
+ req.basic_auth(username,password)
161
+ end
162
+ end
163
+
164
+ def add_oauth(conn,req,auth)
165
+ options = auth.reject do |key,value|
166
+ [:type,:consumer_key,:consumer_secret,:token,:token_secret].include?(key)
167
+ end
168
+ unless options.has_key?(:site)
169
+ options[:site] = oauth_site(conn,req)
170
+ end
171
+ consumer = OAuth::Consumer.new(auth[:consumer_key],auth[:consumer_secret],options)
172
+ access_token = OAuth::AccessToken.new(consumer,auth[:token],auth[:token_secret])
173
+ consumer.sign!(req,access_token)
174
+ end
175
+
176
+ def oauth_site(conn,req)
177
+ site = "#{(conn.use_ssl? ? "https" : "http")}://#{conn.address}"
178
+ if (conn.use_ssl? && conn.port != 443) || (!conn.use_ssl? && conn.port != 80)
179
+ site << ":#{conn.port}"
180
+ end
181
+ site
182
+ end
183
+
184
+ def dump_request(req)
185
+ puts "Sending Request"
186
+ puts"#{req.method} #{req.path}"
187
+ dump_headers(req)
188
+ end
189
+
190
+ def dump_response(res)
191
+ puts "Received Response"
192
+ dump_headers(res)
193
+ puts res.body
194
+ end
195
+
196
+ def dump_headers(msg)
197
+ msg.each_header do |key, value|
198
+ puts "\t#{key}=#{value}"
199
+ end
200
+ end
201
+
202
+ def http_class
203
+ if proxy
204
+ if proxy.kind_of?(Proc)
205
+ proxy.call(self)
206
+ else
207
+ proxy
208
+ end
209
+ else
210
+ Net::HTTP
211
+ end
212
+ end
213
+
214
+ def configure_ssl(conn)
215
+ if self.class.ca_cert_file
216
+ conn.verify_mode = OpenSSL::SSL::VERIFY_PEER
217
+ conn.ca_file = self.class.ca_cert_file
218
+ else
219
+ # Turn off SSL verification which gets rid of warning in 1.8.x and
220
+ # an error in 1.9.x.
221
+ conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
222
+ unless @ssl_warning_shown
223
+ puts <<-EOS
224
+ Warning: SSL Verification is not being performed. While your communication is
225
+ being encrypted, the identity of the other party is not being confirmed nor the
226
+ SSL certificate verified. It's recommended that you specify a file containing
227
+ root SSL certificates like so:
228
+
229
+ Grackle::Transport.ca_cert_file = "path/to/cacerts.pem"
230
+
231
+ You can download this kind of file from the maintainers of cURL:
232
+ http://curl.haxx.se/ca/cacert.pem
233
+
234
+ EOS
235
+ @ssl_warning_shown = true
236
+ end
237
+ end
238
+ end
239
+
240
+ # Methods like Twitter's DELETE list membership expect that the user id
241
+ # will be form encoded like a POST request in the body. Net::HTTP seems
242
+ # to think that DELETEs can't have body parameters so we have to work
243
+ # around that.
244
+ def request_body_permitted?(req)
245
+ req.request_body_permitted? || req.kind_of?(Net::HTTP::Delete)
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,16 @@
1
+ module Grackle
2
+ module Utils
3
+
4
+ VALID_PROFILE_IMAGE_SIZES = [:bigger,:normal,:mini]
5
+
6
+ #Easy method for getting different sized profile images using Twitter's naming scheme
7
+ def profile_image_url(url,size=:normal)
8
+ size = VALID_PROFILE_IMAGE_SIZES.find(:normal){|s| s == size.to_sym}
9
+ return url if url.nil? || size == :normal
10
+ url.sub(/_normal\./,"_#{size.to_s}.")
11
+ end
12
+
13
+ module_function :profile_image_url
14
+
15
+ end
16
+ end
@@ -0,0 +1,359 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class 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
+
359
+ end