koala 1.0.0 → 1.1.0rc

Sign up to get free protection for your applications and to get access to all the features.
@@ -46,6 +46,8 @@ module Koala
46
46
  oauth = Koala::Facebook::OAuth.new(@app_id, @secret)
47
47
  @app_access_token = oauth.get_app_access_token
48
48
  end
49
+
50
+ @graph_api = GraphAPI.new(@app_access_token)
49
51
  end
50
52
 
51
53
  # subscribes for realtime updates
@@ -59,7 +61,7 @@ module Koala
59
61
  :verify_token => verify_token
60
62
  }
61
63
  # a subscription is a success if Facebook returns a 200 (after hitting your server for verification)
62
- api(subscription_path, args, 'post', :http_component => :status) == 200
64
+ @graph_api.graph_call(subscription_path, args, 'post', :http_component => :status) == 200
63
65
  end
64
66
 
65
67
  # removes subscription for object
@@ -67,24 +69,13 @@ module Koala
67
69
  def unsubscribe(object = nil)
68
70
  args = {}
69
71
  args[:object] = object if object
70
- api(subscription_path, args, 'delete', :http_component => :status) == 200
72
+ @graph_api.graph_call(subscription_path, args, 'delete', :http_component => :status) == 200
71
73
  end
72
74
 
73
75
  def list_subscriptions
74
- api(subscription_path)["data"]
76
+ @graph_api.graph_call(subscription_path)["data"]
75
77
  end
76
78
 
77
- def api(*args) # same as GraphAPI
78
- response = super(*args) do |response|
79
- # check for subscription errors
80
- if response.is_a?(Hash) && error_details = response["error"]
81
- raise APIError.new(error_details)
82
- end
83
- end
84
-
85
- response
86
- end
87
-
88
79
  protected
89
80
 
90
81
  def subscription_path
@@ -3,21 +3,26 @@ module Koala
3
3
  REST_SERVER = "api.facebook.com"
4
4
 
5
5
  module RestAPIMethods
6
- def fql_query(fql)
7
- rest_call('fql.query', 'query' => fql)
6
+ def fql_query(fql, args = {}, options = {})
7
+ rest_call('fql.query', args.merge(:query => fql), options)
8
8
  end
9
9
 
10
- def rest_call(method, args = {}, options = {})
11
- options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(method))
10
+ def fql_multiquery(queries = {}, args = {}, options = {})
11
+ if results = rest_call('fql.multiquery', args.merge(:queries => queries.to_json), options)
12
+ # simplify the multiquery result format
13
+ results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
14
+ end
15
+ end
12
16
 
13
- response = api("method/#{method}", args.merge('format' => 'json'), 'get', options) do |response|
17
+ def rest_call(fb_method, args = {}, options = {}, method = "get")
18
+ options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(fb_method.to_s))
19
+
20
+ api("method/#{fb_method}", args.merge('format' => 'json'), method, options) do |response|
14
21
  # check for REST API-specific errors
15
22
  if response.is_a?(Hash) && response["error_code"]
16
23
  raise APIError.new("type" => response["error_code"], "message" => response["error_msg"])
17
24
  end
18
25
  end
19
-
20
- response
21
26
  end
22
27
 
23
28
  # read-only methods for which we can use API-read
@@ -87,4 +92,4 @@ module Koala
87
92
  end
88
93
 
89
94
  end # module Facebook
90
- end # module Koala
95
+ end # module Koala
@@ -2,9 +2,9 @@ require 'koala'
2
2
 
3
3
  module Koala
4
4
  class UploadableIO
5
- attr_reader :io_or_path, :content_type
5
+ attr_reader :io_or_path, :content_type, :requires_base_http_service
6
6
 
7
- def initialize(io_or_path_or_mixed, content_type = nil)
7
+ def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
8
8
  # see if we got the right inputs
9
9
  if content_type.nil?
10
10
  parse_init_mixed_param io_or_path_or_mixed
@@ -13,19 +13,35 @@ module Koala
13
13
  @content_type = content_type
14
14
  end
15
15
 
16
+ # Probably a StringIO or similar object, which won't work with Typhoeus
17
+ @requires_base_http_service = @io_or_path.respond_to?(:read) && !@io_or_path.kind_of?(File)
18
+
19
+ # filename is used in the Ads API
20
+ @filename = filename || "koala-io-file.dum"
21
+
16
22
  raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
17
23
  raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type && Koala.multipart_requires_content_type?
18
24
  end
19
25
 
20
26
  def to_upload_io
21
- UploadIO.new(@io_or_path, @content_type, "koala-io-file.dum")
27
+ UploadIO.new(@io_or_path, @content_type, @filename)
22
28
  end
23
29
 
24
30
  def to_file
25
31
  @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
26
32
  end
33
+
34
+ def self.binary_content?(content)
35
+ content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
36
+ end
27
37
 
28
38
  private
39
+ DETECTION_STRATEGIES = [
40
+ :sinatra_param?,
41
+ :rails_3_param?,
42
+ :file_param?
43
+ ]
44
+
29
45
  PARSE_STRATEGIES = [
30
46
  :parse_rails_3_param,
31
47
  :parse_sinatra_param,
@@ -41,24 +57,36 @@ module Koala
41
57
  end
42
58
 
43
59
  # Expects a parameter of type ActionDispatch::Http::UploadedFile
60
+ def self.rails_3_param?(uploaded_file)
61
+ uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
62
+ end
63
+
44
64
  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)
65
+ if UploadableIO.rails_3_param?(uploaded_file)
46
66
  @io_or_path = uploaded_file.tempfile.path
47
67
  @content_type = uploaded_file.content_type
48
68
  end
49
69
  end
50
70
 
51
71
  # Expects a Sinatra hash of file info
72
+ def self.sinatra_param?(file_hash)
73
+ file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
74
+ end
75
+
52
76
  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)
77
+ if UploadableIO.sinatra_param?(file_hash)
54
78
  @io_or_path = file_hash[:tempfile]
55
79
  @content_type = file_hash[:type] || detect_mime_type(tempfile)
56
80
  end
57
81
  end
58
82
 
59
83
  # takes a file object
84
+ def self.file_param?(file)
85
+ file.kind_of?(File)
86
+ end
87
+
60
88
  def parse_file_object(file)
61
- if file.kind_of?(File)
89
+ if UploadableIO.file_param?(file)
62
90
  @io_or_path = file
63
91
  @content_type = detect_mime_type(file.path)
64
92
  end
@@ -112,4 +140,4 @@ module Koala
112
140
  end
113
141
  end
114
142
  end
115
- end
143
+ end
data/readme.md CHANGED
@@ -1,21 +1,24 @@
1
1
  Koala
2
2
  ====
3
- Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.com/arsduo/koala</a>) is a new Facebook library for Ruby, supporting the Graph API (including photo uploads), the old REST API, realtime updates, and OAuth validation. We wrote Koala with four goals:
3
+ Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.com/arsduo/koala</a>) is a new Facebook library for Ruby, supporting the Graph API (including the batch requests and photo uploads), the REST API, realtime updates, test users, and OAuth validation. We wrote Koala with four goals:
4
4
 
5
5
  * Lightweight: Koala should be as light and simple as Facebook’s own new libraries, providing API accessors and returning simple JSON. (We clock in, with comments, just over 750 lines of code.)
6
6
  * Fast: Koala should, out of the box, be quick. In addition to supporting the vanilla Ruby networking libraries, it natively supports Typhoeus, our preferred gem for making fast HTTP requests. Of course, That brings us to our next topic:
7
7
  * Flexible: Koala should be useful to everyone, regardless of their current configuration. (We have no dependencies beyond the JSON gem. Koala also has a built-in mechanism for using whichever HTTP library you prefer to make requests against the graph.)
8
8
  * Tested: Koala should have complete test coverage, so you can rely on it. (Our complete test coverage can be run against either mocked responses or the live Facebook servers.)
9
9
 
10
- 1.0
10
+ Installation
11
11
  ---
12
- Version 1.0 is due out on May 1st, 2011 with a ton of great features.
12
+
13
+ Easy:
14
+
15
+ [sudo|rvm] gem install koala
16
+ # for 1.1rc add --pre
13
17
 
14
- sudo gem install koala
15
18
 
16
- Until then, you can install the release candidate like so:
19
+ Or in Bundler:
17
20
 
18
- sudo gem install koala --pre
21
+ gem "koala" # add ', "~> 1.1rc"' for the release candidate
19
22
 
20
23
  Graph API
21
24
  ----
@@ -47,6 +50,15 @@ When retrieving data that returns an array of results (for example, when calling
47
50
  # You can use those params to easily get the next (or prevous) page
48
51
  page = graph.get_page(feed.next_page_params)
49
52
 
53
+ You can make multiple calls at once using Facebook's batch API:
54
+
55
+ # Returns an array of results as if they were called non-batch
56
+ graph.batch do
57
+ graph.get_connections('me', 'friends')
58
+ graph.get_object('me')
59
+ graph.get_picture('me')
60
+ end
61
+
50
62
  Check out the wiki for more examples.
51
63
 
52
64
  The old-school REST API
@@ -140,4 +152,4 @@ You can also run live tests against Facebook's servers:
140
152
  # Again from anywhere in the project directory:
141
153
  LIVE=true rake spec
142
154
 
143
- Important Note: to run the live tests, you have to provide some of your own data in spec/fixtures/facebook_data.yml: a valid OAuth access token with publish\_stream, read\_stream, and user\_photos permissions and an OAuth code that can be used to generate an access token. You can get thisdata at the OAuth Playground; if you want to use your own app, remember to swap out the app ID, secret, and other values. (The file also provides valid values for other tests, which you're welcome to swap out for data specific to your own application.)
155
+ Important Note: to run the live tests, you have to provide some of your own data in spec/fixtures/facebook_data.yml: a valid OAuth access token with publish\_stream, read\_stream, and user\_photos permissions and an OAuth code that can be used to generate an access token. You can get thisdata at the OAuth Playground; if you want to use your own app, remember to swap out the app ID, secret, and other values. (The file also provides valid values for other tests, which you're welcome to swap out for data specific to your own application.)
@@ -57,14 +57,14 @@ describe "Koala::Facebook::API" do
57
57
  @service.api('anything').should == json_body
58
58
  end
59
59
 
60
- it "should execute a block with the response body if passed one" do
60
+ it "should execute an error checking block if provided" do
61
61
  body = '{}'
62
62
  Koala.stub(:make_request).and_return(Koala::Response.new(200, body, {}))
63
63
 
64
64
  yield_test = mock('Yield Tester')
65
65
  yield_test.should_receive(:pass)
66
66
 
67
- @service.api('anything') do |arg|
67
+ @service.api('anything', {}, "get") do |arg|
68
68
  yield_test.pass
69
69
  arg.should == JSON.parse(body)
70
70
  end
@@ -0,0 +1,600 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Koala::Facebook::GraphAPI in batch mode" do
4
+ include LiveTestingDataHelper
5
+ before :each do
6
+ @api = Koala::Facebook::GraphAPI.new(@token)
7
+ # app API
8
+ @oauth_data = $testing_data["oauth_test_data"]
9
+ @app_id = @oauth_data["app_id"]
10
+ @app_access_token = @oauth_data["app_access_token"]
11
+ @app_api = Koala::Facebook::GraphAPI.new(@app_access_token)
12
+ end
13
+
14
+ describe "BatchOperations" do
15
+ before :each do
16
+ @args = {
17
+ :url => "my url",
18
+ :args => {:a => 2, :b => 3},
19
+ :method => "get",
20
+ :access_token => "12345",
21
+ :http_options => {},
22
+ :post_processing => lambda { }
23
+ }
24
+ end
25
+
26
+ describe "#new" do
27
+ it "makes http_options accessible" do
28
+ Koala::Facebook::BatchOperation.new(@args).http_options.should == @args[:http_options]
29
+ end
30
+
31
+ it "makes post_processing accessible" do
32
+ Koala::Facebook::BatchOperation.new(@args).post_processing.should == @args[:post_processing]
33
+ end
34
+
35
+ it "makes access_token accessible" do
36
+ Koala::Facebook::BatchOperation.new(@args).access_token.should == @args[:access_token]
37
+ end
38
+
39
+ it "doesn't change the original http_options" do
40
+ @args[:http_options][:name] = "baz2"
41
+ expected = @args[:http_options].dup
42
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)
43
+ @args[:http_options].should == expected
44
+ end
45
+
46
+ it "leaves the file array nil by default" do
47
+ Koala::Facebook::BatchOperation.new(@args).files.should be_nil
48
+ end
49
+
50
+ it "raises a KoalaError if no access token supplied" do
51
+ expect { Koala::Facebook::BatchOperation.new(@args.merge(:access_token => nil)) }.to raise_exception(Koala::KoalaError)
52
+ end
53
+
54
+ describe "when supplied binary files" do
55
+ before :each do
56
+ @binary = stub("Binary file")
57
+ @uploadable_io = stub("UploadableIO 1")
58
+
59
+ @batch_queue = []
60
+ Koala::Facebook::GraphAPI.stub(:batch_calls).and_return(@batch_queue)
61
+
62
+ Koala::UploadableIO.stub(:new).with(@binary).and_return(@uploadable_io)
63
+ Koala::UploadableIO.stub(:binary_content?).and_return(false)
64
+ Koala::UploadableIO.stub(:binary_content?).with(@binary).and_return(true)
65
+ Koala::UploadableIO.stub(:binary_content?).with(@uploadable_io).and_return(true)
66
+ @uploadable_io.stub(:is_a?).with(Koala::UploadableIO).and_return(true)
67
+
68
+ @args[:method] = "post" # files are always post
69
+ end
70
+
71
+ it "adds binary files to the files attribute as UploadableIOs" do
72
+ @args[:args].merge!("source" => @binary)
73
+ batch_op = Koala::Facebook::BatchOperation.new(@args)
74
+ batch_op.files.should_not be_nil
75
+ batch_op.files.find {|k, v| v == @uploadable_io}.should_not be_nil
76
+ end
77
+
78
+ it "works if supplied an UploadableIO as an argument" do
79
+ # as happens with put_picture at the moment
80
+ @args[:args].merge!("source" => @uploadable_io)
81
+ batch_op = Koala::Facebook::BatchOperation.new(@args)
82
+ batch_op.files.should_not be_nil
83
+ batch_op.files.find {|k, v| v == @uploadable_io}.should_not be_nil
84
+ end
85
+
86
+ it "assigns each binary parameter unique name" do
87
+ @args[:args].merge!("source" => @binary, "source2" => @binary)
88
+ batch_op = Koala::Facebook::BatchOperation.new(@args)
89
+ # if the name wasn't unique, there'd just be one item
90
+ batch_op.files.should have(2).items
91
+ end
92
+
93
+ it "assigns each binary parameter unique name across batch requests" do
94
+ @args[:args].merge!("source" => @binary, "source2" => @binary)
95
+ batch_op = Koala::Facebook::BatchOperation.new(@args)
96
+ # simulate the batch operation, since it's used in determination
97
+ @batch_queue << batch_op
98
+ batch_op2 = Koala::Facebook::BatchOperation.new(@args)
99
+ @batch_queue << batch_op2
100
+ # if the name wasn't unique, we should have < 4 items since keys would be the same
101
+ batch_op.files.merge(batch_op2.files).should have(4).items
102
+ end
103
+
104
+ it "removes the value from the arguments" do
105
+ @args[:args].merge!("source" => @binary)
106
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)[:body].should_not =~ /source=/
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ describe ".to_batch_params" do
113
+ describe "handling arguments and URLs" do
114
+ shared_examples_for "request with no body" do
115
+ it "adds the args to the URL string, with ? if no args previously present" do
116
+ test_args = "foo"
117
+ @args[:url] = url = "/"
118
+ Koala.http_service.stub(:encode_params).and_return(test_args)
119
+
120
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)[:relative_url].should == "#{url}?#{test_args}"
121
+ end
122
+
123
+ it "adds the args to the URL string, with & if args previously present" do
124
+ test_args = "foo"
125
+ @args[:url] = url = "/?a=2"
126
+ Koala.http_service.stub(:encode_params).and_return(test_args)
127
+
128
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)[:relative_url].should == "#{url}&#{test_args}"
129
+ end
130
+
131
+ it "adds nothing to the URL string if there are no args to be added" do
132
+ @args[:args] = {}
133
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(@args[:access_token])[:relative_url].should == @args[:url]
134
+ end
135
+
136
+ it "adds nothing to the body" do
137
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)[:body].should be_nil
138
+ end
139
+ end
140
+
141
+ shared_examples_for "requests with a body param" do
142
+ it "sets the body to the encoded args string, if there are args" do
143
+ test_args = "foo"
144
+ Koala.http_service.stub(:encode_params).and_return(test_args)
145
+
146
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)[:body].should == test_args
147
+ end
148
+
149
+ it "does not set the body if there are no args" do
150
+ test_args = ""
151
+ Koala.http_service.stub(:encode_params).and_return(test_args)
152
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)[:body].should be_nil
153
+ end
154
+
155
+
156
+ it "doesn't change the url" do
157
+ test_args = "foo"
158
+ Koala.http_service.stub(:encode_params).and_return(test_args)
159
+
160
+ Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)[:relative_url].should == @args[:url]
161
+ end
162
+ end
163
+
164
+ context "for get operations" do
165
+ before :each do
166
+ @args[:method] = :get
167
+ end
168
+
169
+ it_should_behave_like "request with no body"
170
+ end
171
+
172
+ context "for delete operations" do
173
+ before :each do
174
+ @args[:method] = :delete
175
+ end
176
+
177
+ it_should_behave_like "request with no body"
178
+ end
179
+
180
+ context "for get operations" do
181
+ before :each do
182
+ @args[:method] = :put
183
+ end
184
+
185
+ it_should_behave_like "requests with a body param"
186
+ end
187
+
188
+ context "for delete operations" do
189
+ before :each do
190
+ @args[:method] = :post
191
+ end
192
+
193
+ it_should_behave_like "requests with a body param"
194
+ end
195
+ end
196
+
197
+ it "includes the access token if the token is not the main one for the request" do
198
+ params = Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)
199
+ params[:relative_url].should =~ /access_token=#{@args[:access_token]}/
200
+ end
201
+
202
+ it "includes the other arguments if the token is not the main one for the request" do
203
+ @args[:args] = {:a => 2}
204
+ params = Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)
205
+ params[:relative_url].should =~ /a=2/
206
+ end
207
+
208
+ it "does not include the access token if the token is the main one for the request" do
209
+ params = Koala::Facebook::BatchOperation.new(@args).to_batch_params(@args[:access_token])
210
+ params[:relative_url].should_not =~ /access_token=#{@args[:access_token]}/
211
+ end
212
+
213
+ it "includes the other arguments if the token is the main one for the request" do
214
+ @args[:args] = {:a => 2}
215
+ params = Koala::Facebook::BatchOperation.new(@args).to_batch_params(@args[:access_token])
216
+ params[:relative_url].should =~ /a=2/
217
+ end
218
+
219
+ it "includes any arguments passed as http_options[:batch_args]" do
220
+ batch_args = {:name => "baz", :headers => {:some_param => true}}
221
+ @args[:http_options][:batch_args] = batch_args
222
+ params = Koala::Facebook::BatchOperation.new(@args).to_batch_params(nil)
223
+ params.should include(batch_args)
224
+ end
225
+
226
+ it "includes the method" do
227
+ params = Koala::Facebook::BatchOperation.new(@args).to_batch_params(@args[:access_token])
228
+ params[:method].should == @args[:method].to_sym
229
+ end
230
+
231
+ it "works with nil http_options" do
232
+ expect { Koala::Facebook::BatchOperation.new(@args.merge(:http_options => nil)).to_batch_params(nil) }.not_to raise_exception
233
+ end
234
+
235
+ it "works with nil args" do
236
+ expect { Koala::Facebook::BatchOperation.new(@args.merge(:args => nil)).to_batch_params(nil) }.not_to raise_exception
237
+ end
238
+
239
+ describe "with binary files" do
240
+ before :each do
241
+ @binary = stub("Binary file")
242
+ Koala::UploadableIO.stub(:binary_content?).and_return(false)
243
+ Koala::UploadableIO.stub(:binary_content?).with(@binary).and_return(true)
244
+ @uploadable_io = stub("UploadableIO")
245
+ Koala::UploadableIO.stub(:new).with(@binary).and_return(@uploadable_io)
246
+ @uploadable_io.stub(:is_a?).with(Koala::UploadableIO).and_return(true)
247
+
248
+ @batch_queue = []
249
+ Koala::Facebook::GraphAPI.stub(:batch_calls).and_return(@batch_queue)
250
+
251
+ @args[:method] = "post" # files are always post
252
+ end
253
+
254
+ it "adds file identifiers as attached_files in a comma-separated list" do
255
+ @args[:args].merge!("source" => @binary, "source2" => @binary)
256
+ batch_op = Koala::Facebook::BatchOperation.new(@args)
257
+ file_ids = batch_op.files.find_all {|k, v| v == @uploadable_io}.map {|k, v| k}
258
+ params = batch_op.to_batch_params(nil)
259
+ params[:attached_files].should == file_ids.join(",")
260
+ end
261
+ end
262
+ end
263
+
264
+ end
265
+
266
+ describe "GraphAPI batch interface" do
267
+ it "sets the batch_mode flag to false outside batch mode" do
268
+ Koala::Facebook::GraphAPI.batch_mode?.should be_false
269
+ end
270
+
271
+ it "sets the batch_mode flag inside batch mode" do
272
+ Koala::Facebook::GraphAPI.batch do
273
+ Koala::Facebook::GraphAPI.batch_mode?.should be_true
274
+ end
275
+ end
276
+
277
+ it "throws an error if you try to access the batch_calls queue outside a batch block" do
278
+ expect { Koala::Facebook::GraphAPI.batch_calls << BatchOperation.new(:access_token => "2") }.to raise_exception(Koala::KoalaError)
279
+ end
280
+
281
+ it "clears the batch queue between requests" do
282
+ Koala.stub(:make_request).and_return(Koala::Response.new(200, "[]", {}))
283
+ Koala::Facebook::GraphAPI.batch { @api.get_object("me") }
284
+ Koala.should_receive(:make_request).once.and_return(Koala::Response.new(200, "[]", {}))
285
+ Koala::Facebook::GraphAPI.batch { @api.get_object("me") }
286
+ end
287
+
288
+ it "returns nothing for a batch operation" do
289
+ Koala.stub(:make_request).and_return(Koala::Response.new(200, "[]", {}))
290
+ Koala::Facebook::GraphAPI.batch do
291
+ @api.get_object("me").should be_nil
292
+ end
293
+ end
294
+
295
+ it "creates a BatchObject when making a GraphAPI request in batch mode" do
296
+ Koala.should_receive(:make_request).once.and_return(Koala::Response.new(200, "[]", {}))
297
+
298
+ args = {:a => :b}
299
+ method = "post"
300
+ http_options = {:option => true}
301
+ url = "/a"
302
+ access_token = "token"
303
+ post_processing = lambda {}
304
+ op = Koala::Facebook::BatchOperation.new(:access_token => access_token, :method => :get, :url => "/")
305
+ Koala::Facebook::BatchOperation.should_receive(:new).with(
306
+ :url => url,
307
+ :args => args,
308
+ :method => method,
309
+ :access_token => access_token,
310
+ :http_options => http_options,
311
+ :post_processing => post_processing
312
+ ).and_return(op)
313
+
314
+ Koala::Facebook::GraphAPI.batch do
315
+ Koala::Facebook::GraphAPI.new(access_token).graph_call(url, args, method, http_options, &post_processing)
316
+ end
317
+ end
318
+
319
+ describe "#batch_api" do
320
+ before :each do
321
+ @fake_response = Koala::Response.new(200, "[]", {})
322
+ Koala.stub(:make_request).and_return(@fake_response)
323
+ end
324
+
325
+ describe "making the request" do
326
+ context "with no calls" do
327
+ it "does not make any requests if batch_calls is empty" do
328
+ Koala.should_not_receive(:make_request)
329
+ Koala::Facebook::GraphAPI.batch {}
330
+ end
331
+
332
+ it "returns []" do
333
+ Koala::Facebook::GraphAPI.batch {}.should == []
334
+ end
335
+ end
336
+
337
+ it "includes the first operation's access token as the main one in the args" do
338
+ access_token = "foo"
339
+ Koala.should_receive(:make_request).with(anything, hash_including("access_token" => access_token), anything, anything).and_return(@fake_response)
340
+ Koala::Facebook::GraphAPI.batch do
341
+ Koala::Facebook::GraphAPI.new(access_token).get_object('me')
342
+ Koala::Facebook::GraphAPI.new("bar").get_object('me')
343
+ end
344
+ end
345
+
346
+ it "sets args['batch'] to a json'd map of all the batch params" do
347
+ access_token = "bar"
348
+ op = Koala::Facebook::BatchOperation.new(:access_token => access_token, :method => :get, :url => "/")
349
+ op.stub(:to_batch_params).and_return({:a => 2})
350
+ Koala::Facebook::BatchOperation.stub(:new).and_return(op)
351
+
352
+ # two requests should generate two batch operations
353
+ expected = [op.to_batch_params(access_token), op.to_batch_params(access_token)].to_json
354
+ Koala.should_receive(:make_request).with(anything, hash_including("batch" => expected), anything, anything).and_return(@fake_response)
355
+ Koala::Facebook::GraphAPI.batch do
356
+ Koala::Facebook::GraphAPI.new(access_token).get_object('me')
357
+ Koala::Facebook::GraphAPI.new(access_token).get_object('me')
358
+ end
359
+ end
360
+
361
+ it "adds any files from the batch operations to the arguments" do
362
+ # stub the batch operation
363
+ # we test above to ensure that files are properly assimilated into the BatchOperation instance
364
+ # right now, we want to make sure that batch_api handles them properly
365
+ @key = "file0_0"
366
+ @uploadable_io = stub("UploadableIO")
367
+ batch_op = stub("Koala Batch Operation", :files => {@key => @uploadable_io}, :to_batch_params => {}, :access_token => "foo")
368
+ Koala::Facebook::BatchOperation.stub(:new).and_return(batch_op)
369
+
370
+ Koala.should_receive(:make_request).with(anything, hash_including(@key => @uploadable_io), anything, anything).and_return(@fake_response)
371
+ Koala::Facebook::GraphAPI.batch do
372
+ Koala::Facebook::GraphAPI.new("bar").put_picture("path/to/file", "image/jpeg")
373
+ end
374
+ end
375
+
376
+ it "preserves operation order" do
377
+ access_token = "bar"
378
+ # two requests should generate two batch operations
379
+ Koala.should_receive(:make_request) do |url, args, method, options|
380
+ # test the batch operations to make sure they appear in the right order
381
+ (args ||= {})["batch"].should =~ /.*me\/farglebarg.*otheruser\/bababa/
382
+ @fake_response
383
+ end
384
+ Koala::Facebook::GraphAPI.batch do
385
+ Koala::Facebook::GraphAPI.new(access_token).get_connections('me', "farglebarg")
386
+ Koala::Facebook::GraphAPI.new(access_token).get_connections('otheruser', "bababa")
387
+ end
388
+ end
389
+
390
+ it "makes a POST request" do
391
+ Koala.should_receive(:make_request).with(anything, anything, "post", anything).and_return(@fake_response)
392
+ Koala::Facebook::GraphAPI.batch do
393
+ Koala::Facebook::GraphAPI.new("foo").get_object('me')
394
+ end
395
+ end
396
+
397
+ it "makes a request to /" do
398
+ Koala.should_receive(:make_request).with("/", anything, anything, anything).and_return(@fake_response)
399
+ Koala::Facebook::GraphAPI.batch do
400
+ Koala::Facebook::GraphAPI.new("foo").get_object('me')
401
+ end
402
+ end
403
+
404
+ it "includes any http options specified at the top level" do
405
+ http_options = {"a" => "baz"}
406
+ Koala.should_receive(:make_request).with(anything, anything, anything, hash_including(http_options)).and_return(@fake_response)
407
+ Koala::Facebook::GraphAPI.batch(http_options) do
408
+ Koala::Facebook::GraphAPI.new("foo").get_object('me')
409
+ end
410
+ end
411
+ end
412
+
413
+ describe "processing the request" do
414
+ it "throws an error if the response is not 200" do
415
+ Koala.stub(:make_request).and_return(Koala::Response.new(500, "[]", {}))
416
+ expect { Koala::Facebook::GraphAPI.batch do
417
+ Koala::Facebook::GraphAPI.new("foo").get_object('me')
418
+ end }.to raise_exception(Koala::Facebook::APIError)
419
+ end
420
+
421
+ it "throws an error if the response is a Batch API-style error" do
422
+ Koala.stub(:make_request).and_return(Koala::Response.new(200, '{"error":190,"error_description":"Error validating access token."}', {}))
423
+ expect { Koala::Facebook::GraphAPI.batch do
424
+ Koala::Facebook::GraphAPI.new("foo").get_object('me')
425
+ end }.to raise_exception(Koala::Facebook::APIError)
426
+ end
427
+
428
+ it "returns the result status if http_component is status" do
429
+ Koala.stub(:make_request).and_return(Koala::Response.new(200, '[{"code":203,"headers":[{"name":"Content-Type","value":"text/javascript; charset=UTF-8"}],"body":"{\"id\":\"1234\"}"}]', {}))
430
+ result = Koala::Facebook::GraphAPI.batch do
431
+ @api.get_object("koppel", {}, :http_component => :status)
432
+ end
433
+ result[0].should == 203
434
+ end
435
+
436
+ it "returns the result headers as a hash if http_component is headers" do
437
+ Koala.stub(:make_request).and_return(Koala::Response.new(200, '[{"code":203,"headers":[{"name":"Content-Type","value":"text/javascript; charset=UTF-8"}],"body":"{\"id\":\"1234\"}"}]', {}))
438
+ result = Koala::Facebook::GraphAPI.batch do
439
+ @api.get_object("koppel", {}, :http_component => :headers)
440
+ end
441
+ result[0].should == {"Content-Type" => "text/javascript; charset=UTF-8"}
442
+ end
443
+ end
444
+
445
+ it "is not available on the GraphAndRestAPI class" do
446
+ Koala::Facebook::GraphAndRestAPI.should_not respond_to(:batch)
447
+ end
448
+ end
449
+ end
450
+
451
+ describe "usage tests" do
452
+ it "should be able get two results at once" do
453
+ me, koppel = Koala::Facebook::GraphAPI.batch do
454
+ @api.get_object('me')
455
+ @api.get_object('koppel')
456
+ end
457
+ me['id'].should_not be_nil
458
+ koppel['id'].should_not be_nil
459
+ end
460
+
461
+
462
+ it "works with GraphAndRestAPI instances" do
463
+ me, koppel = Koala::Facebook::GraphAPI.batch do
464
+ Koala::Facebook::GraphAndRestAPI.new(@api.access_token).get_object('me')
465
+ @api.get_object('koppel')
466
+ end
467
+ me['id'].should_not be_nil
468
+ koppel['id'].should_not be_nil
469
+ end
470
+
471
+ it 'should be able to make mixed calls inside of a batch' do
472
+ me, friends = Koala::Facebook::GraphAPI.batch do
473
+ @api.get_object('me')
474
+ @api.get_connections('me', 'friends')
475
+ end
476
+ me['id'].should_not be_nil
477
+ friends.should be_an(Array)
478
+ end
479
+
480
+ it 'should be able to make a get_picture call inside of a batch' do
481
+ pictures = Koala::Facebook::GraphAPI.batch do
482
+ @api.get_picture('me')
483
+ end
484
+ pictures.first.should_not be_empty
485
+ end
486
+
487
+ it "should handle requests for two different tokens" do
488
+ me, insights = Koala::Facebook::GraphAPI.batch do
489
+ @api.get_object('me')
490
+ @app_api.get_connections(@app_id, 'insights')
491
+ end
492
+ me['id'].should_not be_nil
493
+ insights.should be_an(Array)
494
+ end
495
+
496
+ it "inserts errors in the appropriate place, without breaking other results" do
497
+ failed_insights, koppel = Koala::Facebook::GraphAPI.batch do
498
+ @api.get_connections(@app_id, 'insights')
499
+ @app_api.get_object("koppel")
500
+ end
501
+ failed_insights.should be_a(Koala::Facebook::APIError)
502
+ koppel["id"].should_not be_nil
503
+ end
504
+
505
+ it "handles different request methods" do
506
+ result = @api.put_wall_post("Hello, world, from the test suite batch API!")
507
+ wall_post = result["id"]
508
+
509
+ wall_post, koppel = Koala::Facebook::GraphAPI.batch do
510
+ @api.put_like(wall_post)
511
+ @api.delete_object(wall_post)
512
+ end
513
+ end
514
+
515
+ it "allows FQL" do
516
+ result = Koala::Facebook::GraphAPI.batch do
517
+ @api.graph_call("method/fql.query", {:query=>"select name from user where uid=4"}, "post")
518
+ end
519
+
520
+ fql_result = result[0]
521
+ fql_result[0].should be_a(Hash)
522
+ fql_result[0]["name"].should == "Mark Zuckerberg"
523
+ end
524
+
525
+ describe "binary files" do
526
+ it "posts binary files" do
527
+ file = File.open(File.join(File.dirname(__FILE__), "..", "fixtures", "beach.jpg"))
528
+
529
+ result = Koala::Facebook::GraphAPI.batch do
530
+ @api.put_picture(file)
531
+ end
532
+
533
+ @temporary_object_id = result[0]["id"]
534
+ @temporary_object_id.should_not be_nil
535
+ end
536
+
537
+ it "posts binary files with multiple requests" do
538
+ file = File.open(File.join(File.dirname(__FILE__), "..", "fixtures", "beach.jpg"))
539
+
540
+ results = Koala::Facebook::GraphAPI.batch do
541
+ @api.put_picture(file)
542
+ @api.put_picture(file, {}, "koppel")
543
+ end
544
+ results[0]["id"].should_not be_nil
545
+ results[1]["id"].should_not be_nil
546
+ end
547
+ end
548
+
549
+ describe "relating requests" do
550
+ it "allows you create relationships between requests without omit_response_on_success" do
551
+ results = Koala::Facebook::GraphAPI.batch do
552
+ @api.get_connections("me", "friends", {:limit => 5}, :batch_args => {:name => "get-friends"})
553
+ @api.get_objects("{result=get-friends:$.data.*.id}")
554
+ end
555
+
556
+ results[0].should be_nil
557
+ results[1].should be_an(Hash)
558
+ end
559
+
560
+ it "allows you create relationships between requests with omit_response_on_success" do
561
+ results = Koala::Facebook::GraphAPI.batch do
562
+ @api.get_connections("me", "friends", {:limit => 5}, :batch_args => {:name => "get-friends", :omit_response_on_success => false})
563
+ @api.get_objects("{result=get-friends:$.data.*.id}")
564
+ end
565
+
566
+ results[0].should be_an(Array)
567
+ results[1].should be_an(Hash)
568
+ end
569
+
570
+ it "allows you to create dependencies" do
571
+ me, koppel = Koala::Facebook::GraphAPI.batch do
572
+ @api.get_object("me", {}, :batch_args => {:name => "getme"})
573
+ @api.get_object("koppel", {}, :batch_args => {:depends_on => "getme"})
574
+ end
575
+
576
+ me.should be_nil # gotcha! it's omitted because it's a successfully-executed dependency
577
+ koppel["id"].should_not be_nil
578
+ end
579
+
580
+ it "properly handles dependencies that fail" do
581
+ data, koppel = Koala::Facebook::GraphAPI.batch do
582
+ @api.get_connections(@app_id, 'insights', {}, :batch_args => {:name => "getdata"})
583
+ @api.get_object("koppel", {}, :batch_args => {:depends_on => "getdata"})
584
+ end
585
+
586
+ data.should be_a(Koala::Facebook::APIError)
587
+ koppel.should be_nil
588
+ end
589
+
590
+ it "throws an error for badly-constructed request relationships" do
591
+ expect {
592
+ Koala::Facebook::GraphAPI.batch do
593
+ @api.get_connections("me", "friends", {:limit => 5})
594
+ @api.get_objects("{result=i-dont-exist:$.data.*.id}")
595
+ end
596
+ }.to raise_exception(Koala::Facebook::APIError)
597
+ end
598
+ end
599
+ end
600
+ end