koala 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,18 @@
1
+ v1.2.1
2
+ New methods:
3
+ -- RestAPI.set_app_properties handles JSON-encoding application properties
4
+ Updated methods:
5
+ -- OAuth.get_user_from_cookie works with the new signed cookie format (thanks, gmccreight!)
6
+ -- Beta server URLs are now correct
7
+ -- OAuth.parse_signed_request now raises an informative error if the signed_request is malformed
8
+ Internal improvements:
9
+ -- Koala::Multipart middleware properly encoding nested parameters (hashes) in POSTs
10
+ -- Updated readme, changelog, etc.
11
+ Testing improvements:
12
+ -- Live tests with test users now clean up all fake users they create
13
+ -- Removed duplicate test cases
14
+ -- Live tests with test users no longer delete each object they create, speeding things up
15
+
1
16
  v1.2
2
17
  New methods:
3
18
  -- API is now the main API class, contains both Graph and REST methods
data/koala.gemspec CHANGED
@@ -5,7 +5,7 @@ require 'koala/version'
5
5
  Gem::Specification.new do |s|
6
6
  s.name = %q{koala}
7
7
  s.version = Koala::VERSION
8
- s.date = %q{2011-09-27}
8
+ s.date = %q{2011-10-04}
9
9
 
10
10
  s.summary = %q{A lightweight, flexible library for Facebook with support for the Graph API, the REST API, realtime updates, and OAuth authentication.}
11
11
  s.description = %q{Koala is a lightweight, flexible Ruby SDK for Facebook. It allows read/write access to the social graph via the Graph and REST APIs, as well as support for realtime updates and OAuth and Facebook Connect authentication. Koala is fully tested and supports Net::HTTP and Typhoeus connections out of the box and can accept custom modules for other services.}
@@ -20,7 +20,7 @@ module Koala
20
20
  @http_options ||= {}
21
21
 
22
22
  DEFAULT_MIDDLEWARE = Proc.new do |builder|
23
- builder.request :multipart
23
+ builder.use Koala::MultipartRequest
24
24
  builder.request :url_encoded
25
25
  builder.adapter Faraday.default_adapter
26
26
  end
@@ -28,7 +28,8 @@ module Koala
28
28
  def self.server(options = {})
29
29
  server = "#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
30
30
  server.gsub!(/\.facebook/, "-video.facebook") if options[:video]
31
- "#{options[:use_ssl] ? "https" : "http"}://#{options[:beta] ? "beta." : ""}#{server}"
31
+ server.gsub!(/\.facebook/, ".beta.facebook") if options[:beta]
32
+ "#{options[:use_ssl] ? "https" : "http"}://#{server}"
32
33
  end
33
34
 
34
35
  def self.make_request(path, args, verb, options = {})
@@ -0,0 +1,35 @@
1
+ require 'faraday'
2
+
3
+ module Koala
4
+ class MultipartRequest < Faraday::Request::Multipart
5
+ # Facebook expects nested parameters to be passed in a certain way
6
+ # Based on our testing (https://github.com/arsduo/koala/issues/125),
7
+ # Faraday needs two changes to make that work:
8
+ # 1) [] need to be escaped (e.g. params[foo]=bar ==> params%5Bfoo%5D=bar)
9
+ # 2) such messages need to be multipart-encoded
10
+
11
+ self.mime_type = 'multipart/form-data'.freeze
12
+
13
+ def process_request?(env)
14
+ # if the request values contain any hashes or arrays, multipart it
15
+ super || !!(env[:body].respond_to?(:values) && env[:body].values.find {|v| v.is_a?(Hash) || v.is_a?(Array)})
16
+ end
17
+
18
+
19
+ def process_params(params, prefix = nil, pieces = nil, &block)
20
+ params.inject(pieces || []) do |all, (key, value)|
21
+ key = "#{prefix}%5B#{key}%5D" if prefix
22
+
23
+ case value
24
+ when Array
25
+ values = value.inject([]) { |a,v| a << [nil, v] }
26
+ process_params(values, key, all, &block)
27
+ when Hash
28
+ process_params(value, key, all, &block)
29
+ else
30
+ all << block.call(key, value)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/koala/oauth.rb CHANGED
@@ -27,7 +27,8 @@ module Koala
27
27
 
28
28
  def get_user_from_cookie(cookies)
29
29
  if info = get_user_info_from_cookies(cookies)
30
- string = info["uid"]
30
+ # signed cookie has user_id, unsigned cookie has uid
31
+ string = info["user_id"] || info["uid"]
31
32
  end
32
33
  end
33
34
  alias_method :get_user_from_cookies, :get_user_from_cookie
@@ -85,6 +86,8 @@ module Koala
85
86
  # for a more accurate reference implementation strategy.
86
87
  def parse_signed_request(input)
87
88
  encoded_sig, encoded_envelope = input.split('.', 2)
89
+ raise 'SignedRequest: Invalid (incomplete) signature data' unless encoded_sig && encoded_envelope
90
+
88
91
  signature = base64_url_decode(encoded_sig).unpack("H*").first
89
92
  envelope = MultiJson.decode(base64_url_decode(encoded_envelope))
90
93
 
@@ -94,7 +97,7 @@ module Koala
94
97
  hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope)
95
98
  raise 'SignedRequest: Invalid signature' if (signature != hmac)
96
99
 
97
- return envelope
100
+ envelope
98
101
  end
99
102
 
100
103
  # from session keys
@@ -13,6 +13,11 @@ module Koala
13
13
  results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
14
14
  end
15
15
  end
16
+
17
+ def set_app_properties(properties, args = {}, options = {})
18
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "setAppProperties requires an access token"}) unless @access_token
19
+ rest_call("admin.setAppProperties", args.merge(:properties => MultiJson.encode(properties)), options, "post")
20
+ end
16
21
 
17
22
  def rest_call(fb_method, args = {}, options = {}, method = "get")
18
23
  options = options.merge!(:rest_api => true, :read_only => READ_ONLY_METHODS.include?(fb_method.to_s))
@@ -31,7 +31,7 @@ module Koala
31
31
  # Creates and returns a test user
32
32
  args['installed'] = installed
33
33
  args['permissions'] = (permissions.is_a?(Array) ? permissions.join(",") : permissions) if installed
34
- result = @api.graph_call(accounts_path, args, "post", options)
34
+ @api.graph_call(accounts_path, args, "post", options)
35
35
  end
36
36
 
37
37
  def list
data/lib/koala/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Koala
2
- VERSION = "1.2.0"
2
+ VERSION = "1.2.1"
3
3
  end
data/lib/koala.rb CHANGED
@@ -19,6 +19,7 @@ require 'koala/test_users'
19
19
 
20
20
  # HTTP module so we can communicate with Facebook
21
21
  require 'koala/http_service'
22
+ require 'koala/multipart_request'
22
23
 
23
24
  # add KoalaIO class
24
25
  require 'koala/uploadable_io'
data/readme.md CHANGED
@@ -12,11 +12,7 @@ Koala
12
12
  Facebook Changes on October 1, 2011
13
13
  ---
14
14
 
15
- **Koala 1.2 supports all of Facebook's new authentication schemes**, which will be introduced on October 1, 2011; the old Javascript library and older authentication schemes will be deprecated at the same time.
16
-
17
- To test your application, upgrade to the latest version of Koala (see below) and configure your application according to Facebook's [OAuth 2.0 and HTTPS Migration](https://developers.facebook.com/docs/oauth2-https-migration/) guide. If you have the appropriate calls to get_user_info_from_cookies (apps using the Javascript SDK) and/or parse_signed_params (for Canvas and tab apps), your application should work without a hitch.
18
-
19
- _Note_: in their new secure cookie format, Facebook provides an OAuth code, which Koala automatically exchanges for an access token. Because this involves a call to Facebook's servers, you should consider storing the user's access token in their session and only calling get_user_info_from_cookies when necessary (access_token not present, you discover it's expired, etc.). Otherwise, you'll be calling out to Facebook each time the user loads a page, slowing down your site. (As we figure out best practices for this, we'll update the wiki.)
15
+ Koala 1.2 supports all of Facebook's new authentication schemes, which were introduced on October 1, 2011. If you have the appropriate calls to get_user_info_from_cookies (apps using the Javascript SDK) and/or parse_signed_params (for Canvas and tab apps), your application should work without a hitch. For more information, see Facebook's [OAuth 2.0 and HTTPS Migration](https://developers.facebook.com/docs/oauth2-https-migration/) guide.
20
16
 
21
17
  Installation
22
18
  ---
@@ -94,25 +90,31 @@ Of course, you can use the Graph API methods on the same object -- the power of
94
90
  OAuth
95
91
  -----
96
92
  You can use the Graph and REST APIs without an OAuth access token, but the real magic happens when you provide Facebook an OAuth token to prove you're authenticated. Koala provides an OAuth class to make that process easy:
93
+
97
94
  @oauth = Koala::Facebook::OAuth.new(app_id, app_secret, callback_url)
98
95
 
99
96
  If your application uses Koala and the Facebook [JavaScript SDK](http://github.com/facebook/connect-js) (formerly Facebook Connect), you can use the OAuth class to parse the cookies:
97
+
100
98
  @oauth.get_user_from_cookies(cookies) # gets the user's ID
101
99
  @oauth.get_user_info_from_cookies(cookies) # parses and returns the entire hash
102
100
 
103
101
  And if you have to use the more complicated [redirect-based OAuth process](http://developers.facebook.com/docs/authentication/), Koala helps out there, too:
102
+
104
103
  # generate authenticating URL
105
104
  @oauth.url_for_oauth_code
106
105
  # fetch the access token once you have the code
107
106
  @oauth.get_access_token(code)
108
107
 
109
108
  You can also get your application's own access token, which can be used without a user session for subscriptions and certain other requests:
109
+
110
110
  @oauth.get_app_access_token
111
111
 
112
112
  For those building apps on Facebook, parsing signed requests is simple:
113
+
113
114
  @oauth.parse_signed_request(signed_request_string)
114
115
 
115
116
  Or, if for some horrible reason, you're still using session keys, despair not! It's easy to turn them into shiny, modern OAuth tokens:
117
+
116
118
  @oauth.get_token_from_session_key(session_key)
117
119
  @oauth.get_tokens_from_session_keys(array_of_session_keys)
118
120
 
@@ -155,12 +157,27 @@ We also support the test users API, allowing you to conjure up fake users and co
155
157
  # or, if you want to make a whole community:
156
158
  @test_users.create_network(network_size, is_app_installed, common_permissions)
157
159
 
160
+ Talking to Facebook
161
+ -----
162
+
163
+ Koala uses Faraday to make HTTP requests, which means you have complete control over how your app makes HTTP requests to Facebook. You can set Faraday options globally or pass them in on a per-request (or both):
164
+
165
+ # Set an SSL certificate to avoid Net::HTTP errors
166
+ Koala.http_service.http_options = {
167
+ :ssl => { :ca_path => "/etc/ssl/certs" }
168
+ }
169
+ # or on a per-request basis
170
+ @api.get_object(id, args_hash, { :timeout => 10 })
171
+
172
+ The <a href="https://github.com/arsduo/koala/wiki/HTTP-Services">HTTP Services wiki page</a> has more information on what options are available, as well as on how to configure your own Faraday middleware stack (for instance, to implement request logging).
173
+
158
174
  See examples, ask questions
159
175
  -----
160
176
  Some resources to help you as you play with Koala and the Graph API:
161
177
 
162
178
  * Complete Koala documentation <a href="http://wiki.github.com/arsduo/koala/">on the wiki</a>
163
179
  * The <a href="http://groups.google.com/group/koala-users">Koala users group</a> on Google Groups, the place for your Koala and API questions
180
+ * Facebook's <a href="http://developers.facebook.com/tools/explorer/">Graph API Explorer</a>, where you can play with the Graph API in your browser
164
181
  * The Koala-powered <a href="http://oauth.twoalex.com" target="_blank">OAuth Playground</a>, where you can easily generate OAuth access tokens and any other data needed to test out the APIs or OAuth
165
182
 
166
183
  Testing
@@ -5,7 +5,7 @@ describe "Koala::Facebook::API" do
5
5
  @service = Koala::Facebook::API.new
6
6
  end
7
7
 
8
- it "should not include an access token if none was given" do
8
+ it "doesn't include an access token if none was given" do
9
9
  Koala.should_receive(:make_request).with(
10
10
  anything,
11
11
  hash_not_including('access_token' => 1),
@@ -16,7 +16,7 @@ describe "Koala::Facebook::API" do
16
16
  @service.api('anything')
17
17
  end
18
18
 
19
- it "should include an access token if given" do
19
+ it "includes an access token if given" do
20
20
  token = 'adfadf'
21
21
  service = Koala::Facebook::API.new token
22
22
 
@@ -30,13 +30,13 @@ describe "Koala::Facebook::API" do
30
30
  service.api('anything')
31
31
  end
32
32
 
33
- it "should have an attr_reader for access token" do
33
+ it "has an attr_reader for access token" do
34
34
  token = 'adfadf'
35
35
  service = Koala::Facebook::API.new token
36
36
  service.access_token.should == token
37
37
  end
38
38
 
39
- it "should get the attribute of a Koala::Response given by the http_component parameter" do
39
+ it "gets the attribute of a Koala::Response given by the http_component parameter" do
40
40
  http_component = :method_name
41
41
 
42
42
  response = mock('Mock KoalaResponse', :body => '', :status => 200)
@@ -47,7 +47,7 @@ describe "Koala::Facebook::API" do
47
47
  @service.api('anything', {}, 'get', :http_component => http_component)
48
48
  end
49
49
 
50
- it "should return the body of the request as JSON if no http_component is given" do
50
+ it "returns the body of the request as JSON if no http_component is given" do
51
51
  response = stub('response', :body => 'body', :status => 200)
52
52
  Koala.stub(:make_request).and_return(response)
53
53
 
@@ -57,7 +57,7 @@ describe "Koala::Facebook::API" do
57
57
  @service.api('anything').should == json_body
58
58
  end
59
59
 
60
- it "should execute an error checking block if provided" do
60
+ it "executes 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
 
@@ -70,13 +70,13 @@ describe "Koala::Facebook::API" do
70
70
  end
71
71
  end
72
72
 
73
- it "should raise an API error if the HTTP response code is greater than or equal to 500" do
73
+ it "raises an API error if the HTTP response code is greater than or equal to 500" do
74
74
  Koala.stub(:make_request).and_return(Koala::Response.new(500, 'response body', {}))
75
75
 
76
76
  lambda { @service.api('anything') }.should raise_exception(Koala::Facebook::APIError)
77
77
  end
78
78
 
79
- it "should handle rogue true/false as responses" do
79
+ it "handles rogue true/false as responses" do
80
80
  Koala.should_receive(:make_request).and_return(Koala::Response.new(200, 'true', {}))
81
81
  @service.api('anything').should be_true
82
82
 
@@ -85,13 +85,13 @@ describe "Koala::Facebook::API" do
85
85
  end
86
86
 
87
87
  describe "with regard to leading slashes" do
88
- it "should add a leading / to the path if not present" do
88
+ it "adds a leading / to the path if not present" do
89
89
  path = "anything"
90
90
  Koala.should_receive(:make_request).with("/#{path}", anything, anything, anything).and_return(Koala::Response.new(200, 'true', {}))
91
91
  @service.api(path)
92
92
  end
93
93
 
94
- it "shouldn't change the path if a leading / is present" do
94
+ it "doesn't change the path if a leading / is present" do
95
95
  path = "/anything"
96
96
  Koala.should_receive(:make_request).with(path, anything, anything, anything).and_return(Koala::Response.new(200, 'true', {}))
97
97
  @service.api(path)
@@ -20,6 +20,7 @@ describe "Koala::HTTPService" do
20
20
  @builder = stub("Faraday connection builder")
21
21
  @builder.stub(:request)
22
22
  @builder.stub(:adapter)
23
+ @builder.stub(:use)
23
24
  end
24
25
 
25
26
  it "is defined" do
@@ -27,7 +28,7 @@ describe "Koala::HTTPService" do
27
28
  end
28
29
 
29
30
  it "adds multipart" do
30
- @builder.should_receive(:request).with(:multipart)
31
+ @builder.should_receive(:use).with(Koala::MultipartRequest)
31
32
  Koala::HTTPService::DEFAULT_MIDDLEWARE.call(@builder)
32
33
  end
33
34
 
@@ -63,12 +64,12 @@ describe "Koala::HTTPService" do
63
64
 
64
65
  it "returns the beta REST server if options[:rest_api]" do
65
66
  server = Koala::HTTPService.server(@options.merge(:rest_api => true))
66
- server.should =~ Regexp.new("beta.#{Koala::Facebook::REST_SERVER}")
67
+ server.should =~ Regexp.new(Koala::Facebook::REST_SERVER.gsub(/\.facebook/, ".beta.facebook"))
67
68
  end
68
69
 
69
70
  it "returns the beta rest server if !options[:rest_api]" do
70
71
  server = Koala::HTTPService.server(@options)
71
- server.should =~ Regexp.new("beta.#{Koala::Facebook::GRAPH_SERVER}")
72
+ server.should =~ Regexp.new(Koala::Facebook::GRAPH_SERVER.gsub(/\.facebook/, ".beta.facebook"))
72
73
  end
73
74
  end
74
75
 
@@ -77,12 +78,12 @@ describe "Koala::HTTPService" do
77
78
  @options = {:video => true}
78
79
  end
79
80
 
80
- it "should return the REST video server if options[:rest_api]" do
81
+ it "returns the REST video server if options[:rest_api]" do
81
82
  server = Koala::HTTPService.server(@options.merge(:rest_api => true))
82
83
  server.should =~ Regexp.new(Koala::Facebook::REST_SERVER.gsub(/\.facebook/, "-video.facebook"))
83
84
  end
84
85
 
85
- it "should return the graph video server if !options[:rest_api]" do
86
+ it "returns the graph video server if !options[:rest_api]" do
86
87
  server = Koala::HTTPService.server(@options)
87
88
  server.should =~ Regexp.new(Koala::Facebook::GRAPH_SERVER.gsub(/\.facebook/, "-video.facebook"))
88
89
  end
@@ -90,11 +91,11 @@ describe "Koala::HTTPService" do
90
91
  end
91
92
 
92
93
  describe "#encode_params" do
93
- it "should return an empty string if param_hash evaluates to false" do
94
+ it "returns an empty string if param_hash evaluates to false" do
94
95
  Koala::HTTPService.encode_params(nil).should == ''
95
96
  end
96
97
 
97
- it "should convert values to JSON if the value is not a String" do
98
+ it "converts values to JSON if the value is not a String" do
98
99
  val = 'json_value'
99
100
  not_a_string = 'not_a_string'
100
101
  not_a_string.stub(:is_a?).and_return(false)
@@ -113,7 +114,7 @@ describe "Koala::HTTPService" do
113
114
  end.should be_true
114
115
  end
115
116
 
116
- it "should escape all values" do
117
+ it "escapes all values" do
117
118
  args = Hash[*(1..4).map {|i| [i.to_s, "Value #{i}($"]}.flatten]
118
119
 
119
120
  result = Koala::HTTPService.encode_params(args)
@@ -123,7 +124,7 @@ describe "Koala::HTTPService" do
123
124
  end
124
125
  end
125
126
 
126
- it "should convert all keys to Strings" do
127
+ it "converts all keys to Strings" do
127
128
  args = Hash[*(1..4).map {|i| [i, "val#{i}"]}.flatten]
128
129
 
129
130
  result = Koala::HTTPService.encode_params(args)
@@ -427,16 +428,16 @@ describe "Koala::HTTPService" do
427
428
  :net_http => Koala::NetHTTPService
428
429
  }.each_pair do |adapter, module_class|
429
430
  describe module_class.to_s do
430
- it "should respond to deprecated_interface" do
431
+ it "responds to deprecated_interface" do
431
432
  module_class.should respond_to(:deprecated_interface)
432
433
  end
433
434
 
434
- it "should issue a deprecation warning" do
435
+ it "issues a deprecation warning" do
435
436
  Koala::Utils.should_receive(:deprecate)
436
437
  module_class.deprecated_interface
437
438
  end
438
439
 
439
- it "should set the default adapter to #{adapter}" do
440
+ it "sets the default adapter to #{adapter}" do
440
441
  module_class.deprecated_interface
441
442
  Faraday.default_adapter.should == adapter
442
443
  end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe Koala::MultipartRequest do
4
+ it "is a subclass of Faraday::Request::Multipart" do
5
+ Koala::MultipartRequest.superclass.should == Faraday::Request::Multipart
6
+ end
7
+
8
+ it "defines mime_type as multipart/form-data" do
9
+ Koala::MultipartRequest.mime_type.should == 'multipart/form-data'
10
+ end
11
+
12
+ describe ".process_request?" do
13
+ before :each do
14
+ @env = {}
15
+ @multipart = Koala::MultipartRequest.new
16
+ @multipart.stub(:request_type).and_return("")
17
+ end
18
+
19
+ # no way to test the call to super, unfortunately
20
+ it "returns true if env[:body] is a hash with at least one hash in its values" do
21
+ @env[:body] = {:a => {:c => 2}}
22
+ @multipart.process_request?(@env).should be_true
23
+ end
24
+
25
+ it "returns true if env[:body] is a hash with at least one array in its values" do
26
+ @env[:body] = {:a => [:c, 2]}
27
+ @multipart.process_request?(@env).should be_true
28
+ end
29
+
30
+ it "returns true if env[:body] is a hash with mixed objects in its values" do
31
+ @env[:body] = {:a => [:c, 2], :b => {:e => :f}}
32
+ @multipart.process_request?(@env).should be_true
33
+ end
34
+
35
+ it "returns false if env[:body] is a string" do
36
+ @env[:body] = "my body"
37
+ @multipart.process_request?(@env).should be_false
38
+ end
39
+
40
+ it "returns false if env[:body] is a hash without an array or hash value" do
41
+ @env[:body] = {:a => 3}
42
+ @multipart.process_request?(@env).should be_false
43
+ end
44
+ end
45
+
46
+ describe ".process_params" do
47
+ before :each do
48
+ @parent = Faraday::Request::Multipart.new
49
+ @multipart = Koala::MultipartRequest.new
50
+ @block = lambda {|k, v| "#{k}=#{v}"}
51
+ end
52
+
53
+ it "is identical to the parent for requests without a prefix" do
54
+ hash = {:a => 2, :c => "3"}
55
+ @multipart.process_params(hash, &@block).should == @parent.process_params(hash, &@block)
56
+ end
57
+
58
+ it "replaces encodes [ and ] if the request has a prefix" do
59
+ hash = {:a => 2, :c => "3"}
60
+ prefix = "foo"
61
+ # process_params returns an array
62
+ @multipart.process_params(hash, prefix, &@block).join("&").should == @parent.process_params(hash, prefix, &@block).join("&").gsub(/\[/, "%5B").gsub(/\]/, "%5D")
63
+ end
64
+ end
65
+
66
+ end