bloveless_grackle 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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