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.
- 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
|