koala 0.10.0 → 1.0.0.rc
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/.gitignore +3 -0
- data/CHANGELOG +39 -7
- data/Gemfile +3 -0
- data/Manifest +8 -1
- data/Rakefile +13 -14
- data/koala.gemspec +36 -19
- data/lib/koala/graph_api.rb +188 -123
- data/lib/koala/http_services.rb +93 -18
- data/lib/koala/rest_api.rb +73 -6
- data/lib/koala/test_users.rb +21 -8
- data/lib/koala/uploadable_io.rb +115 -0
- data/lib/koala.rb +86 -86
- data/readme.md +18 -12
- data/spec/cases/api_base_spec.rb +101 -0
- data/spec/cases/graph_and_rest_api_spec.rb +31 -0
- data/spec/cases/graph_api_spec.rb +25 -0
- data/spec/cases/http_services/http_service_spec.rb +54 -0
- data/spec/cases/http_services/net_http_service_spec.rb +350 -0
- data/spec/cases/http_services/typhoeus_service_spec.rb +144 -0
- data/spec/cases/oauth_spec.rb +374 -0
- data/spec/cases/realtime_updates_spec.rb +184 -0
- data/spec/cases/rest_api_spec.rb +25 -0
- data/spec/{koala/test_users/test_users_tests.rb → cases/test_users_spec.rb} +78 -72
- data/spec/cases/uploadable_io_spec.rb +151 -0
- data/spec/fixtures/beach.jpg +0 -0
- data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +13 -9
- data/spec/{mock_facebook_responses.yml → fixtures/mock_facebook_responses.yml} +312 -289
- data/spec/spec_helper.rb +18 -0
- data/spec/support/graph_api_shared_examples.rb +424 -0
- data/spec/{koala → support}/live_testing_data_helper.rb +39 -42
- data/spec/{mock_http_service.rb → support/mock_http_service.rb} +94 -81
- data/spec/support/rest_api_shared_examples.rb +161 -0
- data/spec/support/setup_mocks_or_live.rb +52 -0
- data/spec/support/uploadable_io_shared_examples.rb +76 -0
- metadata +130 -43
- data/init.rb +0 -2
- data/spec/koala/api_base_tests.rb +0 -102
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -10
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -11
- data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -114
- data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -150
- data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
- data/spec/koala/net_http_service_tests.rb +0 -186
- data/spec/koala/oauth/oauth_tests.rb +0 -438
- data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
- data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -94
- data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -36
- data/spec/koala_spec.rb +0 -18
- data/spec/koala_spec_helper.rb +0 -48
- data/spec/koala_spec_without_mocks.rb +0 -19
data/lib/koala/http_services.rb
CHANGED
|
@@ -8,63 +8,138 @@ module Koala
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
module HTTPService
|
|
12
|
+
# common functionality for all HTTP services
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.class_eval do
|
|
15
|
+
class << self
|
|
16
|
+
attr_accessor :always_use_ssl
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.server(options = {})
|
|
20
|
+
"#{options[:beta] ? "beta." : ""}#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def self.params_require_multipart?(param_hash)
|
|
26
|
+
param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.multipart_requires_content_type?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
11
36
|
module NetHTTPService
|
|
12
37
|
# this service uses Net::HTTP to send requests to the graph
|
|
13
38
|
def self.included(base)
|
|
14
39
|
base.class_eval do
|
|
15
|
-
require
|
|
16
|
-
require
|
|
40
|
+
require "net/http" unless defined?(Net::HTTP)
|
|
41
|
+
require "net/https"
|
|
42
|
+
require "net/http/post/multipart"
|
|
43
|
+
|
|
44
|
+
include Koala::HTTPService
|
|
17
45
|
|
|
18
46
|
def self.make_request(path, args, verb, options = {})
|
|
19
47
|
# We translate args to a valid query string. If post is specified,
|
|
20
48
|
# we send a POST request to the given path with the given arguments.
|
|
21
49
|
|
|
50
|
+
# by default, we use SSL only for private requests
|
|
51
|
+
# this makes public requests faster
|
|
52
|
+
private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
|
|
53
|
+
|
|
22
54
|
# if the verb isn't get or post, send it as a post argument
|
|
23
55
|
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
|
24
56
|
|
|
25
|
-
|
|
26
|
-
http =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
http = create_http(server(options), private_request, options)
|
|
58
|
+
http.use_ssl = true if private_request
|
|
59
|
+
|
|
60
|
+
result = http.start do |http|
|
|
61
|
+
response, body = if verb == "post"
|
|
62
|
+
if params_require_multipart? args
|
|
63
|
+
http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
|
|
64
|
+
else
|
|
65
|
+
http.post(path, encode_params(args))
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
http.get("#{path}?#{encode_params(args)}")
|
|
69
|
+
end
|
|
70
|
+
|
|
36
71
|
Koala::Response.new(response.code.to_i, body, response)
|
|
37
|
-
|
|
72
|
+
end
|
|
38
73
|
end
|
|
39
74
|
|
|
40
75
|
protected
|
|
41
76
|
def self.encode_params(param_hash)
|
|
42
77
|
# unfortunately, we can't use to_query because that's Rails, not Ruby
|
|
43
78
|
# if no hash (e.g. no auth token) return empty string
|
|
44
|
-
((param_hash || {}).collect do |key_and_value|
|
|
79
|
+
((param_hash || {}).collect do |key_and_value|
|
|
45
80
|
key_and_value[1] = key_and_value[1].to_json if key_and_value[1].class != String
|
|
46
81
|
"#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
|
|
47
82
|
end).join("&")
|
|
48
83
|
end
|
|
84
|
+
|
|
85
|
+
def self.encode_multipart_params(param_hash)
|
|
86
|
+
Hash[*param_hash.collect do |key, value|
|
|
87
|
+
[key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
|
|
88
|
+
end.flatten]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.create_http(server, private_request, options)
|
|
92
|
+
if options[:proxy]
|
|
93
|
+
proxy = URI.parse(options[:proxy])
|
|
94
|
+
http = Net::HTTP.new(server, private_request ? 443 : nil,
|
|
95
|
+
proxy.host, proxy.port, proxy.user, proxy.password)
|
|
96
|
+
else
|
|
97
|
+
http = Net::HTTP.new(server, private_request ? 443 : nil)
|
|
98
|
+
end
|
|
99
|
+
if options[:timeout]
|
|
100
|
+
http.open_timeout = options[:timeout]
|
|
101
|
+
http.read_timeout = options[:timeout]
|
|
102
|
+
end
|
|
103
|
+
http
|
|
104
|
+
end
|
|
105
|
+
|
|
49
106
|
end
|
|
50
107
|
end
|
|
51
108
|
end
|
|
52
109
|
|
|
53
110
|
module TyphoeusService
|
|
54
111
|
# this service uses Typhoeus to send requests to the graph
|
|
112
|
+
|
|
55
113
|
def self.included(base)
|
|
56
114
|
base.class_eval do
|
|
57
|
-
require
|
|
115
|
+
require "typhoeus" unless defined?(Typhoeus)
|
|
58
116
|
include Typhoeus
|
|
117
|
+
|
|
118
|
+
include Koala::HTTPService
|
|
59
119
|
|
|
60
120
|
def self.make_request(path, args, verb, options = {})
|
|
61
121
|
# if the verb isn't get or post, send it as a post argument
|
|
62
122
|
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
|
63
|
-
|
|
123
|
+
|
|
124
|
+
# switch any UploadableIOs to the files Typhoeus expects
|
|
125
|
+
args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
|
|
126
|
+
|
|
127
|
+
# you can pass arguments directly to Typhoeus using the :typhoeus_options key
|
|
64
128
|
typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
|
|
65
|
-
|
|
129
|
+
|
|
130
|
+
# by default, we use SSL only for private requests (e.g. with access token)
|
|
131
|
+
# this makes public requests faster
|
|
132
|
+
prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
|
|
133
|
+
|
|
134
|
+
response = self.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
|
|
66
135
|
Koala::Response.new(response.code, response.body, response.headers_hash)
|
|
67
136
|
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def self.multipart_requires_content_type?
|
|
141
|
+
false # Typhoeus handles multipart file types, we don't have to require it
|
|
142
|
+
end
|
|
68
143
|
end # class_eval
|
|
69
144
|
end
|
|
70
145
|
end
|
data/lib/koala/rest_api.rb
CHANGED
|
@@ -1,23 +1,90 @@
|
|
|
1
1
|
module Koala
|
|
2
2
|
module Facebook
|
|
3
3
|
REST_SERVER = "api.facebook.com"
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
module RestAPIMethods
|
|
6
6
|
def fql_query(fql)
|
|
7
7
|
rest_call('fql.query', 'query' => fql)
|
|
8
8
|
end
|
|
9
|
-
|
|
10
|
-
def rest_call(method, args = {})
|
|
11
|
-
|
|
9
|
+
|
|
10
|
+
def rest_call(method, args = {}, options = {})
|
|
11
|
+
options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(method))
|
|
12
|
+
|
|
13
|
+
response = api("method/#{method}", args.merge('format' => 'json'), 'get', options) do |response|
|
|
12
14
|
# check for REST API-specific errors
|
|
13
15
|
if response.is_a?(Hash) && response["error_code"]
|
|
14
16
|
raise APIError.new("type" => response["error_code"], "message" => response["error_msg"])
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
|
-
|
|
19
|
+
|
|
18
20
|
response
|
|
19
21
|
end
|
|
22
|
+
|
|
23
|
+
# read-only methods for which we can use API-read
|
|
24
|
+
# taken directly from the FB PHP library (https://github.com/facebook/php-sdk/blob/master/src/facebook.php)
|
|
25
|
+
READ_ONLY_METHODS = [
|
|
26
|
+
'admin.getallocation',
|
|
27
|
+
'admin.getappproperties',
|
|
28
|
+
'admin.getbannedusers',
|
|
29
|
+
'admin.getlivestreamvialink',
|
|
30
|
+
'admin.getmetrics',
|
|
31
|
+
'admin.getrestrictioninfo',
|
|
32
|
+
'application.getpublicinfo',
|
|
33
|
+
'auth.getapppublickey',
|
|
34
|
+
'auth.getsession',
|
|
35
|
+
'auth.getsignedpublicsessiondata',
|
|
36
|
+
'comments.get',
|
|
37
|
+
'connect.getunconnectedfriendscount',
|
|
38
|
+
'dashboard.getactivity',
|
|
39
|
+
'dashboard.getcount',
|
|
40
|
+
'dashboard.getglobalnews',
|
|
41
|
+
'dashboard.getnews',
|
|
42
|
+
'dashboard.multigetcount',
|
|
43
|
+
'dashboard.multigetnews',
|
|
44
|
+
'data.getcookies',
|
|
45
|
+
'events.get',
|
|
46
|
+
'events.getmembers',
|
|
47
|
+
'fbml.getcustomtags',
|
|
48
|
+
'feed.getappfriendstories',
|
|
49
|
+
'feed.getregisteredtemplatebundlebyid',
|
|
50
|
+
'feed.getregisteredtemplatebundles',
|
|
51
|
+
'fql.multiquery',
|
|
52
|
+
'fql.query',
|
|
53
|
+
'friends.arefriends',
|
|
54
|
+
'friends.get',
|
|
55
|
+
'friends.getappusers',
|
|
56
|
+
'friends.getlists',
|
|
57
|
+
'friends.getmutualfriends',
|
|
58
|
+
'gifts.get',
|
|
59
|
+
'groups.get',
|
|
60
|
+
'groups.getmembers',
|
|
61
|
+
'intl.gettranslations',
|
|
62
|
+
'links.get',
|
|
63
|
+
'notes.get',
|
|
64
|
+
'notifications.get',
|
|
65
|
+
'pages.getinfo',
|
|
66
|
+
'pages.isadmin',
|
|
67
|
+
'pages.isappadded',
|
|
68
|
+
'pages.isfan',
|
|
69
|
+
'permissions.checkavailableapiaccess',
|
|
70
|
+
'permissions.checkgrantedapiaccess',
|
|
71
|
+
'photos.get',
|
|
72
|
+
'photos.getalbums',
|
|
73
|
+
'photos.gettags',
|
|
74
|
+
'profile.getinfo',
|
|
75
|
+
'profile.getinfooptions',
|
|
76
|
+
'stream.get',
|
|
77
|
+
'stream.getcomments',
|
|
78
|
+
'stream.getfilters',
|
|
79
|
+
'users.getinfo',
|
|
80
|
+
'users.getloggedinuser',
|
|
81
|
+
'users.getstandardinfo',
|
|
82
|
+
'users.hasapppermission',
|
|
83
|
+
'users.isappuser',
|
|
84
|
+
'users.isverified',
|
|
85
|
+
'video.getuploadlimits'
|
|
86
|
+
]
|
|
20
87
|
end
|
|
21
|
-
|
|
88
|
+
|
|
22
89
|
end # module Facebook
|
|
23
90
|
end # module Koala
|
data/lib/koala/test_users.rb
CHANGED
|
@@ -20,11 +20,11 @@ module Koala
|
|
|
20
20
|
@graph_api = GraphAPI.new(@app_access_token)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def create(installed, permissions = nil)
|
|
23
|
+
def create(installed, permissions = nil, args = {}, options = {})
|
|
24
24
|
# Creates and returns a test user
|
|
25
|
-
args
|
|
25
|
+
args['installed'] = installed
|
|
26
26
|
args['permissions'] = (permissions.is_a?(Array) ? permissions.join(",") : permissions) if installed
|
|
27
|
-
result = @graph_api.graph_call(accounts_path, args, "post")
|
|
27
|
+
result = @graph_api.graph_call(accounts_path, args, "post", options)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def list
|
|
@@ -40,12 +40,25 @@ module Koala
|
|
|
40
40
|
list.each {|u| delete u }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def befriend(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
def befriend(user1_hash, user2_hash)
|
|
44
|
+
user1_id = user1_hash["id"] || user1_hash[:id]
|
|
45
|
+
user2_id = user2_hash["id"] || user2_hash[:id]
|
|
46
|
+
user1_token = user1_hash["access_token"] || user1_hash[:access_token]
|
|
47
|
+
user2_token = user2_hash["access_token"] || user2_hash[:access_token]
|
|
48
|
+
unless user1_id && user2_id && user1_token && user2_token
|
|
49
|
+
# we explicitly raise an error here to minimize the risk of confusing output
|
|
50
|
+
# if you pass in a string (as was previously supported) no local exception would be raised
|
|
51
|
+
# but the Facebook call would fail
|
|
52
|
+
raise ArgumentError, "TestUsers#befriend requires hash arguments for both users with id and access_token"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
u1_graph_api = GraphAPI.new(user1_token)
|
|
56
|
+
u2_graph_api = GraphAPI.new(user2_token)
|
|
57
|
+
|
|
58
|
+
u1_graph_api.graph_call("#{user1_id}/friends/#{user2_id}", {}, "post") &&
|
|
59
|
+
u2_graph_api.graph_call("#{user2_id}/friends/#{user1_id}", {}, "post")
|
|
47
60
|
end
|
|
48
|
-
|
|
61
|
+
|
|
49
62
|
def create_network(network_size, installed = true, permissions = '')
|
|
50
63
|
network_size = 50 if network_size > 50 # FB's max is 50
|
|
51
64
|
users = (0...network_size).collect { create(installed, permissions) }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require 'koala'
|
|
2
|
+
|
|
3
|
+
module Koala
|
|
4
|
+
class UploadableIO
|
|
5
|
+
attr_reader :io_or_path, :content_type
|
|
6
|
+
|
|
7
|
+
def initialize(io_or_path_or_mixed, content_type = nil)
|
|
8
|
+
# see if we got the right inputs
|
|
9
|
+
if content_type.nil?
|
|
10
|
+
parse_init_mixed_param io_or_path_or_mixed
|
|
11
|
+
elsif !content_type.nil? && (io_or_path_or_mixed.respond_to?(:read) or io_or_path_or_mixed.kind_of?(String))
|
|
12
|
+
@io_or_path = io_or_path_or_mixed
|
|
13
|
+
@content_type = content_type
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
|
|
17
|
+
raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type && Koala.multipart_requires_content_type?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_upload_io
|
|
21
|
+
UploadIO.new(@io_or_path, @content_type, "koala-io-file.dum")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_file
|
|
25
|
+
@io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
PARSE_STRATEGIES = [
|
|
30
|
+
:parse_rails_3_param,
|
|
31
|
+
:parse_sinatra_param,
|
|
32
|
+
:parse_file_object,
|
|
33
|
+
:parse_string_path
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def parse_init_mixed_param(mixed)
|
|
37
|
+
PARSE_STRATEGIES.each do |method|
|
|
38
|
+
send(method, mixed)
|
|
39
|
+
return if @io_or_path && @content_type
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Expects a parameter of type ActionDispatch::Http::UploadedFile
|
|
44
|
+
def parse_rails_3_param(uploaded_file)
|
|
45
|
+
if uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
|
|
46
|
+
@io_or_path = uploaded_file.tempfile.path
|
|
47
|
+
@content_type = uploaded_file.content_type
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Expects a Sinatra hash of file info
|
|
52
|
+
def parse_sinatra_param(file_hash)
|
|
53
|
+
if file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
|
|
54
|
+
@io_or_path = file_hash[:tempfile]
|
|
55
|
+
@content_type = file_hash[:type] || detect_mime_type(tempfile)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# takes a file object
|
|
60
|
+
def parse_file_object(file)
|
|
61
|
+
if file.kind_of?(File)
|
|
62
|
+
@io_or_path = file
|
|
63
|
+
@content_type = detect_mime_type(file.path)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def parse_string_path(path)
|
|
68
|
+
if path.kind_of?(String)
|
|
69
|
+
@io_or_path = path
|
|
70
|
+
@content_type = detect_mime_type(path)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
MIME_TYPE_STRATEGIES = [
|
|
75
|
+
:use_mime_module,
|
|
76
|
+
:use_simple_detection
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def detect_mime_type(filename)
|
|
80
|
+
if filename
|
|
81
|
+
MIME_TYPE_STRATEGIES.each do |method|
|
|
82
|
+
result = send(method, filename)
|
|
83
|
+
return result if result
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
nil # if we can't find anything
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def use_mime_module(filename)
|
|
90
|
+
# if the user has installed mime/types, we can use that
|
|
91
|
+
# if not, rescue and return nil
|
|
92
|
+
begin
|
|
93
|
+
type = MIME::Types.type_for(filename).first
|
|
94
|
+
type ? type.to_s : nil
|
|
95
|
+
rescue
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def use_simple_detection(filename)
|
|
101
|
+
# very rudimentary extension analysis for images
|
|
102
|
+
# first, get the downcased extension, or an empty string if it doesn't exist
|
|
103
|
+
extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
|
|
104
|
+
if extension == ""
|
|
105
|
+
nil
|
|
106
|
+
elsif extension == "jpg" || extension == "jpeg"
|
|
107
|
+
"image/jpeg"
|
|
108
|
+
elsif extension == "png"
|
|
109
|
+
"image/png"
|
|
110
|
+
elsif extension == "gif"
|
|
111
|
+
"image/gif"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|