bloveless_grackle 0.1.10
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +46 -0
- data/README.rdoc +217 -0
- data/grackle.gemspec +40 -0
- data/lib/grackle.rb +31 -0
- data/lib/grackle/client.rb +295 -0
- data/lib/grackle/handlers.rb +90 -0
- data/lib/grackle/transport.rb +248 -0
- data/lib/grackle/utils.rb +16 -0
- data/test/test_client.rb +359 -0
- data/test/test_grackle.rb +4 -0
- data/test/test_handlers.rb +89 -0
- data/test/test_helper.rb +3 -0
- metadata +112 -0
@@ -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
|
data/test/test_client.rb
ADDED
@@ -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
|