koala 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/readme.md CHANGED
@@ -10,16 +10,38 @@ Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.co
10
10
  Graph API
11
11
  ----
12
12
  The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
13
+
13
14
  graph = Koala::Facebook::GraphAPI.new(oauth_access_token)
14
15
  profile = graph.get_object("me")
15
- friends = graph.get_connection("me", "friends")
16
+ friends = graph.get_connections("me", "friends")
16
17
  graph.put_object("me", "feed", :message => "I am writing on my wall!")
17
18
 
19
+ The response of most requests is the JSON data returned from the Facebook servers as a Hash.
20
+
21
+ When retrieving data that returns an array of results (for example, when calling GraphAPI#get_connections or GraphAPI#search) a GraphCollection object (a sub-class of Array) will be returned, which contains added methods for getting the next and previous page of results:
22
+
23
+ # Returns the feed items for the currently logged-in user as a GraphCollection
24
+ feed = graph.get_connections("me", "feed")
25
+
26
+ # GraphCollection is a sub-class of Array, so you can use it as a usual Array
27
+ first_entry = feed[0]
28
+ last_entry = feed.last
29
+
30
+ # Returns the next page of results (also as a GraphCollection)
31
+ next_feed = feed.next_page
32
+
33
+ # Returns an array describing the URL for the next page: [path, arguments]
34
+ # This is useful for paging across multiple requests
35
+ next_path, next_args = feed.next_page_params
36
+
37
+ # You can use those params to easily get the next (or prevous) page
38
+ page = graph.get_page(feed.next_page_params)
39
+
18
40
  Check out the wiki for more examples.
19
41
 
20
42
  The old-school REST API
21
43
  -----
22
- Where the Graph API and the old REST API overlap, you should choose the Graph API. Unfortunately, that overlap is far from complete, and there are many important API calls -- including fql.query -- that can't yet be done via the Graph.
44
+ Where the Graph API and the old REST API overlap, you should choose the Graph API. Unfortunately, that overlap is far from complete, and there are many important API calls that can't yet be done via the Graph.
23
45
 
24
46
  Koala now supports the old-school REST API using OAuth access tokens; to use this, instantiate your class using the RestAPI class:
25
47
 
@@ -40,16 +62,19 @@ If your application uses Koala and the Facebook [JavaScript SDK](http://github.c
40
62
  @oauth.get_user_from_cookie(cookies)
41
63
 
42
64
  And if you have to use the more complicated [redirect-based OAuth process](http://developers.facebook.com/docs/authentication/), Koala helps out there, too:
43
- # generate authenticating URL
44
- @oauth.url_for_oauth_code
45
- # fetch the access token once you have the code
46
- @oauth.get_access_token(code)
65
+ # generate authenticating URL
66
+ @oauth.url_for_oauth_code
67
+ # fetch the access token once you have the code
68
+ @oauth.get_access_token(code)
47
69
 
48
70
  You can also get your application's own access token, which can be used without a user session for subscriptions and certain other requests:
49
71
  @oauth.get_app_access_token
50
72
 
51
73
  That's it! It's pretty simple once you get the hang of it. If you're new to OAuth, though, check out the wiki and the OAuth Playground example site (see below).
52
74
 
75
+ *Signed Requests:* Excited to try out the new signed request authentication scheme? Good news! Koala now supports parsing those parameters:
76
+ @oauth.parse_signed_request(request)
77
+
53
78
  *Exchanging session keys:* Stuck building tab applications on Facebook? Wishing you had an OAuth token so you could use the Graph API? You're in luck! Koala now allows you to exchange session keys for OAuth access tokens:
54
79
  @oauth.get_token_from_session_key(session_key)
55
80
  @oauth.get_tokens_from_session_keys(array_of_session_keys)
@@ -60,11 +85,11 @@ The Graph API now allows your application to subscribe to real-time updates for
60
85
 
61
86
  Currently, Facebook only supports subscribing to users, permissions and errors. On top of that, there are limitations on what attributes and connections for each of these objects you can subscribe to updates for. Check the [official Facebook documentation](http://developers.facebook.com/docs/api/realtime) for more details.
62
87
 
63
- Koala makes it easy to interact with your applications using the RealTimeUpdates class:
88
+ Koala makes it easy to interact with your applications using the RealtimeUpdates class:
64
89
 
65
- @updates = Koala::Facebook::RealTimeUpdates.new(:app_id => app_id, :secret => secret)
90
+ @updates = Koala::Facebook::RealtimeUpdates.new(:app_id => app_id, :secret => secret)
66
91
 
67
- You can do just about anything with your real-time update subscriptions using the RealTimeUpdates class:
92
+ You can do just about anything with your real-time update subscriptions using the RealtimeUpdates class:
68
93
 
69
94
  # Add/modify a subscription to updates for when the first_name or last_name fields of any of your users is changed
70
95
  @updates.subscribe("user", "first_name, last_name", callback_token, verify_token)
@@ -75,12 +100,12 @@ You can do just about anything with your real-time update subscriptions using th
75
100
  # Unsubscribe from updates for an object
76
101
  @updates.unsubscribe("user")
77
102
 
78
- And to top it all off, RealTimeUpdates provides a static method to respond to Facebook servers' verification of your callback URLs:
103
+ And to top it all off, RealtimeUpdates provides a static method to respond to Facebook servers' verification of your callback URLs:
79
104
 
80
105
  # Returns the hub.challenge parameter in params if the verify token in params matches verify_token
81
- Koala::Facebook::RealTimeUpdates.meet_challenge(params, your_verify_token)
106
+ Koala::Facebook::RealtimeUpdates.meet_challenge(params, your_verify_token)
82
107
 
83
- For more information about meet_challenge and the RealTimeUpdates class, check out the Real-Time Updates page on the wiki.
108
+ For more information about meet_challenge and the RealtimeUpdates class, check out the Real-Time Updates page on the wiki.
84
109
 
85
110
  See examples, ask questions
86
111
  -----
@@ -5,18 +5,18 @@
5
5
 
6
6
  # You must supply this value yourself to test the GraphAPI class.
7
7
  # Your OAuth token should have publish_stream and read_stream permissions.
8
- oauth_token:
8
+ oauth_token: 119908831367602|2.K0IVdhrRngS7VQM4Z_s6_g__.3600.1285844400-2905623|Q3VNUb9haS3s29X4SYPk6VL1f9A
9
9
 
10
10
  # for testing the OAuth class
11
11
  # baseline app
12
12
  oauth_test_data:
13
13
  # You must supply this value yourself, since they will expire.
14
- code:
14
+ code: 2.K0IVdhrRngS7VQM4Z_s6_g__.3600.1285844400-2905623|b5PCXOhEMe2FJXPHAg_mY3Psl6M
15
15
  # easiest way to get session keys: use multiple test accounts with the Javascript login at http://oauth.twoalex.com
16
- session_key:
16
+ session_key: 2.K0IVdhrRngS7VQM4Z_s6_g__.3600.1285844400-2905623
17
17
  multiple_session_keys:
18
- -
19
- -
18
+ - 2.K0IVdhrRngS7VQM4Z_s6_g__.3600.1285844400-2905623
19
+ - 2.K0IVdhrRngS7VQM4Z_s6_g__.3600.1285844400-2905623
20
20
 
21
21
  # These values will work out of the box
22
22
  app_id: 119908831367602
@@ -34,7 +34,19 @@ oauth_test_data:
34
34
  offline_access_cookies:
35
35
  # note: I've revoked the offline access for security reasons, so you can't make calls against this :)
36
36
  fbs_119908831367602: '"access_token=119908831367602|08170230801eb225068e7a70-2905623|Q3LDCYYF8CX9cstxnZLsxiR0nwg.&expires=0&secret=78abaee300b392e275072a9f2727d436&session_key=08170230801eb225068e7a70-2905623&sig=423b8aa4b6fa1f9a571955f8e929d567&uid=2905623"'
37
+
38
+ # These values will work out of the box
39
+ # They're from Facebook's example at http://developers.facebook.com/docs/authentication/canvas
40
+ # You can update this to live data if desired
41
+ # request_secret is optional and will fall back to the secret above if absent
42
+ request_secret: "secret"
43
+ signed_request: "vlXgu64BQGFSQrY0ZcJBZASMvYvTHu9GQ0YM9rjPSso.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsIjAiOiJwYXlsb2FkIn0"
44
+ signed_request_result:
45
+ "0": payload
46
+ algorithm: HMAC-SHA256
37
47
 
48
+
49
+
38
50
  subscription_test_data:
39
51
  subscription_path: http://oauth.twoalex.com/subscriptions
40
52
  verify_token: "myverificationtoken|1f54545d5f722733e17faae15377928f"
@@ -76,5 +76,20 @@ class ApiBaseTests < Test::Unit::TestCase
76
76
  Koala.should_receive(:make_request).and_return(Koala::Response.new(200, 'false', {}))
77
77
  @service.api('anything').should be_false
78
78
  end
79
+
80
+ describe "with regard to leading slashes" do
81
+ it "should add a leading / to the path if not present" do
82
+ path = "anything"
83
+ Koala.should_receive(:make_request).with("/#{path}", anything, anything, anything).and_return(Koala::Response.new(200, 'true', {}))
84
+ @service.api(path)
85
+ end
86
+
87
+ it "shouldn't change the path if a leading / is present" do
88
+ path = "/anything"
89
+ Koala.should_receive(:make_request).with(path, anything, anything, anything).and_return(Koala::Response.new(200, 'true', {}))
90
+ @service.api(path)
91
+ end
92
+ end
93
+
79
94
  end
80
95
  end
@@ -39,9 +39,17 @@ shared_examples_for "Koala GraphAPI without an access token" do
39
39
  end
40
40
 
41
41
  it "should be able to access connections from public Pages" do
42
- result = @api.get_connections("contextoptional", "likes")
42
+ result = @api.get_connections("contextoptional", "photos")
43
43
  result.should be_a(Array)
44
44
  end
45
+
46
+ # paging
47
+ # see also graph_collection_tests
48
+ it "should make a request for a page when provided a specific set of page params" do
49
+ query = [1, 2]
50
+ @api.should_receive(:graph_call).with(*query)
51
+ @api.get_page(query)
52
+ end
45
53
 
46
54
  it "should not be able to put an object" do
47
55
  lambda { @result = @api.put_object("lukeshepard", "feed", :message => "Hello, world") }.should raise_error(Koala::Facebook::APIError)
@@ -66,8 +74,7 @@ shared_examples_for "Koala GraphAPI without an access token" do
66
74
  it "should not be able to like an object" do
67
75
  lambda { @api.put_like("7204941866_119776748033392") }.should raise_error(Koala::Facebook::APIError)
68
76
  end
69
-
70
-
77
+
71
78
  # DELETE
72
79
  it "should not be able to delete posts" do
73
80
  # test post on the Ruby SDK Test application
@@ -77,9 +84,11 @@ shared_examples_for "Koala GraphAPI without an access token" do
77
84
  # SEARCH
78
85
  it "should be able to search" do
79
86
  result = @api.search("facebook")
80
- result["data"].should be_an(Array)
87
+ result.length.should be_an(Integer)
81
88
  end
82
89
 
90
+ it_should_behave_like "Koala GraphAPI with GraphCollection"
91
+
83
92
  # API
84
93
  it "should never use the rest api server" do
85
94
  Koala.should_receive(:make_request).with(
@@ -1,5 +1,5 @@
1
1
  shared_examples_for "Koala GraphAPI with an access token" do
2
- it "should get public data about a user" do
2
+ it "should get public data about a user" do
3
3
  result = @api.get_object("koppel")
4
4
  # the results should have an ID and a name, among other things
5
5
  (result["id"] && result["name"]).should_not be_nil
@@ -41,10 +41,19 @@ it "should get public data about a user" do
41
41
  end
42
42
 
43
43
  it "should be able to access connections from public Pages" do
44
- result = @api.get_connections("contextoptional", "likes")
44
+ result = @api.get_connections("contextoptional", "photos")
45
45
  result.should be_a(Array)
46
46
  end
47
47
 
48
+ # paging
49
+ # see also graph_collection_tests
50
+ it "should make a request for a page when provided a specific set of page params" do
51
+ query = [1, 2]
52
+ @api.should_receive(:graph_call).with(*query)
53
+ @api.get_page(query)
54
+ end
55
+
56
+
48
57
  # PUT
49
58
  it "should be able to write an object to the graph" do
50
59
  result = @api.put_wall_post("Hello, world, from the test suite!")
@@ -120,11 +129,13 @@ it "should get public data about a user" do
120
129
  # SEARCH
121
130
  it "should be able to search" do
122
131
  result = @api.search("facebook")
123
- result["data"].should be_an(Array)
132
+ result.length.should be_an(Integer)
124
133
  end
125
134
 
126
135
  # API
127
136
  # the above tests test this already, but we should consider additional api tests
137
+
138
+ it_should_behave_like "Koala GraphAPI with GraphCollection"
128
139
  end
129
140
 
130
141
  class FacebookWithAccessTokenTests < Test::Unit::TestCase
@@ -0,0 +1,104 @@
1
+ # GraphCollection
2
+ shared_examples_for "Koala GraphAPI with GraphCollection" do
3
+
4
+ it "should create an array-like object" do
5
+ call = @api.graph_call("contextoptional/photos")
6
+ Koala::Facebook::GraphCollection.new(call, @api).should be_an(Array)
7
+ end
8
+
9
+ describe "when getting a collection" do
10
+ # GraphCollection methods
11
+ it "should get a GraphCollection when getting connections" do
12
+ @result = @api.get_connections("contextoptional", "photos")
13
+ @result.should be_a(Koala::Facebook::GraphCollection)
14
+ end
15
+
16
+ it "should return nil if the get_collections call fails with nil" do
17
+ # this happens sometimes
18
+ @api.should_receive(:graph_call).and_return(nil)
19
+ @api.get_connections("contextoptional", "photos").should be_nil
20
+ end
21
+
22
+ it "should get a GraphCollection when searching" do
23
+ result = @api.search("facebook")
24
+ result.should be_a(Koala::Facebook::GraphCollection)
25
+ end
26
+
27
+ it "should return nil if the search call fails with nil" do
28
+ # this happens sometimes
29
+ @api.should_receive(:graph_call).and_return(nil)
30
+ @api.search("facebook").should be_nil
31
+ end
32
+
33
+ it "should get a GraphCollection when paging through results" do
34
+ @results = @api.get_page(["search", {"q"=>"facebook", "limit"=>"25", "until"=>"2010-09-23T21:17:33+0000"}])
35
+ @results.should be_a(Koala::Facebook::GraphCollection)
36
+ end
37
+
38
+ it "should return nil if the page call fails with nil" do
39
+ # this happens sometimes
40
+ @api.should_receive(:graph_call).and_return(nil)
41
+ @api.get_page(["search", {"q"=>"facebook", "limit"=>"25", "until"=>"2010-09-23T21:17:33+0000"}]).should be_nil
42
+ end
43
+
44
+ # GraphCollection attributes
45
+ describe "the GraphCollection" do
46
+ before(:each) do
47
+ @result = @api.get_connections("contextoptional", "photos")
48
+ end
49
+
50
+ it "should have a read-only paging attribute" do
51
+ lambda { @result.paging }.should_not raise_error
52
+ lambda { @result.paging = "paging" }.should raise_error(NoMethodError)
53
+ end
54
+
55
+ describe "when getting a whole page" do
56
+ before(:each) do
57
+ @second_page = stub("page of Fb graph results")
58
+ @base = stub("base")
59
+ @args = stub("args")
60
+ @page_of_results = stub("page of results")
61
+ end
62
+
63
+ it "should return the previous page of results" do
64
+ @result.should_receive(:previous_page_params).and_return([@base, @args])
65
+ @api.should_receive(:graph_call).with(@base, @args).and_return(@second_page)
66
+ Koala::Facebook::GraphCollection.should_receive(:new).with(@second_page, @api).and_return(@page_of_results)
67
+
68
+ @result.previous_page(@api).should == @page_of_results
69
+ end
70
+
71
+ it "should return the next page of results" do
72
+ @result.should_receive(:next_page_params).and_return([@base, @args])
73
+ @api.should_receive(:graph_call).with(@base, @args).and_return(@second_page)
74
+ Koala::Facebook::GraphCollection.should_receive(:new).with(@second_page, @api).and_return(@page_of_results)
75
+
76
+ @result.next_page.should == @page_of_results
77
+ end
78
+
79
+ it "should return nil it there are no other pages" do
80
+ %w{next previous}.each do |this|
81
+ @result.should_receive("#{this}_page_params".to_sym).and_return(nil)
82
+ @result.send("#{this}_page", @api).should == nil
83
+ end
84
+ end
85
+ end
86
+
87
+ describe "when parsing page paramters" do
88
+ before(:each) do
89
+ @graph_collection = Koala::Facebook::GraphCollection.new({"data" => []}, Koala::Facebook::GraphAPI.new)
90
+ end
91
+
92
+ it "should return the base as the first array entry" do
93
+ base = "url_path"
94
+ @graph_collection.parse_page_url("anything.com/#{base}?anything").first.should == base
95
+ end
96
+
97
+ it "should return the arguments as a hash as the last array entry" do
98
+ args_hash = {"one" => "val_one", "two" => "val_two"}
99
+ @graph_collection.parse_page_url("anything.com/anything?#{args_hash.map {|k,v| "#{k}=#{v}" }.join("&")}").last.should == args_hash
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -74,7 +74,7 @@ class NetHTTPServiceTests < Test::Unit::TestCase
74
74
  Bear.make_request('anything', {}, 'post')
75
75
  end
76
76
 
77
- it "should go to the specified path" do
77
+ it "should go to the specified path adding a / if it doesn't exist" do
78
78
  path = mock('Path')
79
79
  @http_yield_mock.should_receive(:post).with(path, anything).and_return(@http_request_result)
80
80
 
@@ -10,7 +10,7 @@ class Time
10
10
  end
11
11
 
12
12
  class FacebookOAuthTests < Test::Unit::TestCase
13
- describe "Koala GraphAPI without an access token" do
13
+ describe "Koala OAuth tests" do
14
14
  before :each do
15
15
  # make the relevant test data easily accessible
16
16
  @oauth_data = $testing_data["oauth_test_data"]
@@ -21,6 +21,13 @@ class FacebookOAuthTests < Test::Unit::TestCase
21
21
  @raw_token_string = @oauth_data["raw_token_string"]
22
22
  @raw_offline_access_token_string = @oauth_data["raw_offline_access_token_string"]
23
23
 
24
+ # per Facebook's example:
25
+ # http://developers.facebook.com/docs/authentication/canvas
26
+ # this allows us to use Facebook's example data while running the other live data
27
+ @request_secret = @oauth_data["request_secret"] || @secret
28
+ @signed_request = @oauth_data["signed_request"]
29
+ @signed_request_result = @oauth_data["signed_request_result"]
30
+
24
31
  # this should expanded to cover all variables
25
32
  raise Exception, "Must supply app data to run FacebookOAuthTests!" unless @app_id && @secret && @callback_url &&
26
33
  @code && @raw_token_string &&
@@ -51,7 +58,7 @@ class FacebookOAuthTests < Test::Unit::TestCase
51
58
  @oauth.oauth_callback_url == nil).should be_true
52
59
  end
53
60
 
54
- describe "cookie parsing" do
61
+ describe "for cookie parsing" do
55
62
  describe "get_user_info_from_cookies" do
56
63
  it "should properly parse valid cookies" do
57
64
  result = @oauth.get_user_info_from_cookies(@oauth_data["valid_cookies"])
@@ -113,113 +120,155 @@ class FacebookOAuthTests < Test::Unit::TestCase
113
120
 
114
121
  # OAuth URLs
115
122
 
116
- # url_for_oauth_code
117
- it "should generate a properly formatted OAuth code URL with the default values" do
118
- url = @oauth.url_for_oauth_code
119
- url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{@callback_url}"
120
- end
121
-
122
- it "should generate a properly formatted OAuth code URL when a callback is given" do
123
- callback = "foo.com"
124
- url = @oauth.url_for_oauth_code(:callback => callback)
125
- url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}"
126
- end
123
+ describe "for URL generation" do
127
124
 
128
- it "should generate a properly formatted OAuth code URL when permissions are requested as a string" do
129
- permissions = "publish_stream,read_stream"
130
- url = @oauth.url_for_oauth_code(:permissions => permissions)
131
- url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{@callback_url}&scope=#{permissions}"
132
- end
125
+ describe "for OAuth codes" do
126
+ # url_for_oauth_code
127
+ it "should generate a properly formatted OAuth code URL with the default values" do
128
+ url = @oauth.url_for_oauth_code
129
+ url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{@callback_url}"
130
+ end
133
131
 
134
- it "should generate a properly formatted OAuth code URL when permissions are requested as a string" do
135
- permissions = ["publish_stream", "read_stream"]
136
- url = @oauth.url_for_oauth_code(:permissions => permissions)
137
- url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{@callback_url}&scope=#{permissions.join(",")}"
138
- end
132
+ it "should generate a properly formatted OAuth code URL when a callback is given" do
133
+ callback = "foo.com"
134
+ url = @oauth.url_for_oauth_code(:callback => callback)
135
+ url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}"
136
+ end
139
137
 
140
- it "should generate a properly formatted OAuth code URL when both permissions and callback are provided" do
141
- permissions = "publish_stream,read_stream"
142
- callback = "foo.com"
143
- url = @oauth.url_for_oauth_code(:callback => callback, :permissions => permissions)
144
- url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}&scope=#{permissions}"
145
- end
138
+ it "should generate a properly formatted OAuth code URL when permissions are requested as a string" do
139
+ permissions = "publish_stream,read_stream"
140
+ url = @oauth.url_for_oauth_code(:permissions => permissions)
141
+ url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{@callback_url}&scope=#{permissions}"
142
+ end
146
143
 
147
- it "should raise an exception if no callback is given in initialization or the call" do
148
- oauth2 = Koala::Facebook::OAuth.new(@app_id, @secret)
149
- lambda { oauth2.url_for_oauth_code }.should raise_error(ArgumentError)
150
- end
144
+ it "should generate a properly formatted OAuth code URL when permissions are requested as a string" do
145
+ permissions = ["publish_stream", "read_stream"]
146
+ url = @oauth.url_for_oauth_code(:permissions => permissions)
147
+ url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{@callback_url}&scope=#{permissions.join(",")}"
148
+ end
151
149
 
152
- # url_for_access_token
153
- it "should generate a properly formatted OAuth token URL when provided a code" do
154
- url = @oauth.url_for_access_token(@code)
155
- url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{@callback_url}&client_secret=#{@secret}&code=#{@code}"
156
- end
150
+ it "should generate a properly formatted OAuth code URL when both permissions and callback are provided" do
151
+ permissions = "publish_stream,read_stream"
152
+ callback = "foo.com"
153
+ url = @oauth.url_for_oauth_code(:callback => callback, :permissions => permissions)
154
+ url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}&scope=#{permissions}"
155
+ end
157
156
 
158
- it "should generate a properly formatted OAuth token URL when provided a callback" do
159
- callback = "foo.com"
160
- url = @oauth.url_for_access_token(@code, :callback => callback)
161
- url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@secret}&code=#{@code}"
162
- end
163
-
164
- describe "get_access_token_info" do
165
- it "should properly get and parse an access token token results into a hash" do
166
- result = @oauth.get_access_token_info(@code)
167
- result.should be_a(Hash)
157
+ it "should raise an exception if no callback is given in initialization or the call" do
158
+ oauth2 = Koala::Facebook::OAuth.new(@app_id, @secret)
159
+ lambda { oauth2.url_for_oauth_code }.should raise_error(ArgumentError)
160
+ end
168
161
  end
162
+
163
+ describe "for access token URLs" do
169
164
 
170
- it "should properly include the access token results" do
171
- result = @oauth.get_access_token_info(@code)
172
- result["access_token"].should
173
- end
165
+ # url_for_access_token
166
+ it "should generate a properly formatted OAuth token URL when provided a code" do
167
+ url = @oauth.url_for_access_token(@code)
168
+ url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{@callback_url}&client_secret=#{@secret}&code=#{@code}"
169
+ end
174
170
 
175
- it "should raise an error when get_access_token is called with a bad code" do
176
- lambda { @oauth.get_access_token_info("foo") }.should raise_error(Koala::Facebook::APIError)
171
+ it "should generate a properly formatted OAuth token URL when provided a callback" do
172
+ callback = "foo.com"
173
+ url = @oauth.url_for_access_token(@code, :callback => callback)
174
+ url.should == "https://#{Koala::Facebook::GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@secret}&code=#{@code}"
175
+ end
177
176
  end
178
177
  end
178
+
179
+ describe "for fetching access tokens" do
180
+ describe "get_access_token_info" do
181
+ it "should properly get and parse an access token token results into a hash" do
182
+ result = @oauth.get_access_token_info(@code)
183
+ result.should be_a(Hash)
184
+ end
179
185
 
180
- describe "get_access_token" do
181
- it "should use get_access_token_info to get and parse an access token token results" do
182
- result = @oauth.get_access_token(@code)
183
- result.should be_a(String)
184
- end
186
+ it "should properly include the access token results" do
187
+ result = @oauth.get_access_token_info(@code)
188
+ result["access_token"].should
189
+ end
185
190
 
186
- it "should return the access token as a string" do
187
- result = @oauth.get_access_token(@code)
188
- original = @oauth.get_access_token_info(@code)
189
- result.should == original["access_token"]
190
- end
191
-
192
- it "should raise an error when get_access_token is called with a bad code" do
193
- lambda { @oauth.get_access_token("foo") }.should raise_error(Koala::Facebook::APIError)
191
+ it "should raise an error when get_access_token is called with a bad code" do
192
+ lambda { @oauth.get_access_token_info("foo") }.should raise_error(Koala::Facebook::APIError)
193
+ end
194
194
  end
195
- end
196
195
 
197
- describe "get_app_access_token_info" do
198
- it "should properly get and parse an app's access token as a hash" do
199
- result = @oauth.get_app_access_token_info
200
- result.should be_a(Hash)
196
+ describe "get_access_token" do
197
+ it "should use get_access_token_info to get and parse an access token token results" do
198
+ result = @oauth.get_access_token(@code)
199
+ result.should be_a(String)
200
+ end
201
+
202
+ it "should return the access token as a string" do
203
+ result = @oauth.get_access_token(@code)
204
+ original = @oauth.get_access_token_info(@code)
205
+ result.should == original["access_token"]
206
+ end
207
+
208
+ it "should raise an error when get_access_token is called with a bad code" do
209
+ lambda { @oauth.get_access_token("foo") }.should raise_error(Koala::Facebook::APIError)
210
+ end
201
211
  end
212
+
213
+ describe "get_app_access_token_info" do
214
+ it "should properly get and parse an app's access token as a hash" do
215
+ result = @oauth.get_app_access_token_info
216
+ result.should be_a(Hash)
217
+ end
202
218
 
203
- it "should include the access token" do
204
- result = @oauth.get_app_access_token_info
205
- result["access_token"].should
219
+ it "should include the access token" do
220
+ result = @oauth.get_app_access_token_info
221
+ result["access_token"].should
222
+ end
206
223
  end
207
- end
208
224
 
209
- describe "get_app_acess_token" do
210
- it "should use get_access_token_info to get and parse an access token token results" do
211
- result = @oauth.get_app_access_token
212
- result.should be_a(String)
225
+ describe "get_app_acess_token" do
226
+ it "should use get_access_token_info to get and parse an access token token results" do
227
+ result = @oauth.get_app_access_token
228
+ result.should be_a(String)
229
+ end
230
+
231
+ it "should return the access token as a string" do
232
+ result = @oauth.get_app_access_token
233
+ original = @oauth.get_app_access_token_info
234
+ result.should == original["access_token"]
235
+ end
213
236
  end
237
+
238
+ describe "protected methods" do
239
+
240
+ # protected methods
241
+ # since these are pretty fundamental and pretty testable, we want to test them
242
+
243
+ # parse_access_token
244
+ it "should properly parse access token results" do
245
+ result = @oauth.send(:parse_access_token, @raw_token_string)
246
+ has_both_parts = result["access_token"] && result["expires"]
247
+ has_both_parts.should
248
+ end
214
249
 
215
- it "should return the access token as a string" do
216
- result = @oauth.get_app_access_token
217
- original = @oauth.get_app_access_token_info
218
- result.should == original["access_token"]
250
+ it "should properly parse offline access token results" do
251
+ result = @oauth.send(:parse_access_token, @raw_offline_access_token_string)
252
+ has_both_parts = result["access_token"] && !result["expires"]
253
+ has_both_parts.should
254
+ end
255
+
256
+ # fetch_token_string
257
+ # somewhat duplicative with the tests for get_access_token and get_app_access_token
258
+ # but no harm in thoroughness
259
+ it "should fetch a proper token string from Facebook when given a code" do
260
+ result = @oauth.send(:fetch_token_string, :code => @code, :redirect_uri => @callback_url)
261
+ result.should =~ /^access_token/
262
+ end
263
+
264
+ it "should fetch a proper token string from Facebook when asked for the app token" do
265
+ result = @oauth.send(:fetch_token_string, {:type => 'client_cred'}, true)
266
+ result.should =~ /^access_token/
267
+ end
219
268
  end
220
269
  end
221
270
 
222
- describe "exchanging session keys" do
271
+ describe "for exchanging session keys" do
223
272
  describe "with get_token_info_from_session_keys" do
224
273
  it "should get an array of session keys from Facebook when passed a single key" do
225
274
  result = @oauth.get_tokens_from_session_keys([@oauth_data["session_key"]])
@@ -237,6 +286,18 @@ class FacebookOAuthTests < Test::Unit::TestCase
237
286
  result = @oauth.get_token_info_from_session_keys(@oauth_data["multiple_session_keys"])
238
287
  result[0].should be_a(Hash)
239
288
  end
289
+
290
+ it "should properly handle invalid session keys" do
291
+ result = @oauth.get_token_info_from_session_keys(["foo", "bar"])
292
+ #it should return nil for each of the invalid ones
293
+ result.each {|r| r.should be_nil}
294
+ end
295
+
296
+ it "should properly handle a mix of valid and invalid session keys" do
297
+ result = @oauth.get_token_info_from_session_keys(["foo"].concat(@oauth_data["multiple_session_keys"]))
298
+ # it should return nil for each of the invalid ones
299
+ result.each_with_index {|r, index| index > 0 ? r.should(be_a(Hash)) : r.should(be_nil)}
300
+ end
240
301
  end
241
302
 
242
303
  describe "with get_tokens_from_session_keys" do
@@ -251,6 +312,18 @@ class FacebookOAuthTests < Test::Unit::TestCase
251
312
  result = @oauth.get_tokens_from_session_keys(args)
252
313
  result.each {|r| r.should be_a(String) }
253
314
  end
315
+
316
+ it "should properly handle invalid session keys" do
317
+ result = @oauth.get_tokens_from_session_keys(["foo", "bar"])
318
+ # it should return nil for each of the invalid ones
319
+ result.each {|r| r.should be_nil}
320
+ end
321
+
322
+ it "should properly handle a mix of valid and invalid session keys" do
323
+ result = @oauth.get_tokens_from_session_keys(["foo"].concat(@oauth_data["multiple_session_keys"]))
324
+ # it should return nil for each of the invalid ones
325
+ result.each_with_index {|r, index| index > 0 ? r.should(be_a(String)) : r.should(be_nil)}
326
+ end
254
327
  end
255
328
 
256
329
  describe "get_token_from_session_key" do
@@ -270,39 +343,98 @@ class FacebookOAuthTests < Test::Unit::TestCase
270
343
  array = @oauth.get_tokens_from_session_keys([@oauth_data["session_key"]])
271
344
  result.should == array[0]
272
345
  end
346
+
347
+ it "should properly handle an invalid session key" do
348
+ result = @oauth.get_token_from_session_key("foo")
349
+ result.should be_nil
350
+ end
273
351
  end
274
352
  end
275
353
 
276
- # protected methods
277
- # since these are pretty fundamental and pretty testable, we want to test them
278
-
279
- # parse_access_token
280
- it "should properly parse access token results" do
281
- result = @oauth.send(:parse_access_token, @raw_token_string)
282
- has_both_parts = result["access_token"] && result["expires"]
283
- has_both_parts.should
284
- end
285
-
286
- it "should properly parse offline access token results" do
287
- result = @oauth.send(:parse_access_token, @raw_offline_access_token_string)
288
- has_both_parts = result["access_token"] && !result["expires"]
289
- has_both_parts.should
290
- end
291
-
292
- # fetch_token_string
293
- # somewhat duplicative with the tests for get_access_token and get_app_access_token
294
- # but no harm in thoroughness
295
- it "should fetch a proper token string from Facebook when given a code" do
296
- result = @oauth.send(:fetch_token_string, :code => @code, :redirect_uri => @callback_url)
297
- result.should =~ /^access_token/
298
- end
354
+ describe "for parsing signed requests" do
355
+ before :each do
356
+ # you can test against live data by updating the YML, or you can use the default data
357
+ # which tests against Facebook's example on http://developers.facebook.com/docs/authentication/canvas
358
+ @oauth = Koala::Facebook::OAuth.new(@app_id, @request_secret || @app_secret)
359
+ end
299
360
 
300
- it "should fetch a proper token string from Facebook when asked for the app token" do
301
- result = @oauth.send(:fetch_token_string, {:type => 'client_cred'}, true)
302
- result.should =~ /^access_token/
361
+ it "should break the request into the encoded signature and the payload" do
362
+ @signed_request.should_receive(:split).with(".").and_return(["", ""])
363
+ @oauth.parse_signed_request(@signed_request)
364
+ end
365
+
366
+ it "should base64 URL decode the signed request" do
367
+ sig = ""
368
+ @signed_request.should_receive(:split).with(".").and_return([sig, "1"])
369
+ @oauth.should_receive(:base64_url_decode).with(sig).and_return("4")
370
+ @oauth.parse_signed_request(@signed_request)
371
+ end
372
+
373
+ it "should base64 URL decode the signed request" do
374
+ sig = @signed_request.split(".")[0]
375
+ @oauth.should_receive(:base64_url_decode).with(sig).and_return(nil)
376
+ @oauth.parse_signed_request(@signed_request)
377
+ end
378
+
379
+ it "should get the sha64 encoded payload using proper arguments from OpenSSL::HMAC" do
380
+ payload = ""
381
+ @signed_request.should_receive(:split).with(".").and_return(["1", payload])
382
+ OpenSSL::HMAC.should_receive(:digest).with("sha256", @request_secret, payload)
383
+ @oauth.parse_signed_request(@signed_request)
384
+ end
385
+
386
+ it "should compare the encoded payload with the signature" do
387
+ sig = "2"
388
+ @oauth.should_receive(:base64_url_decode).and_return(sig)
389
+ encoded_payload = "1"
390
+ OpenSSL::HMAC.should_receive(:digest).with(anything, anything, anything).and_return(encoded_payload)
391
+ encoded_payload.should_receive(:==).with(sig)
392
+ @oauth.parse_signed_request(@signed_request)
393
+ end
394
+
395
+ describe "if the encoded payload matches the signature" do
396
+ before :each do
397
+ # set it up so the sig will match the encoded payload
398
+ raw_sig = ""
399
+ @sig = "2"
400
+ @payload = "1"
401
+ @signed_request.should_receive(:split).and_return([raw_sig, @payload])
402
+ @oauth.should_receive(:base64_url_decode).with(raw_sig).and_return(@sig)
403
+ OpenSSL::HMAC.should_receive(:digest).with(anything, anything, anything).and_return(@sig.dup)
404
+ end
405
+
406
+ it "should base64_url_decode the payload" do
407
+ @oauth.should_receive(:base64_url_decode).with(@payload).ordered.and_return("{}")
408
+ @oauth.parse_signed_request(@signed_request)
409
+ end
410
+
411
+ it "should JSON decode the payload" do
412
+ result = "{}"
413
+ @oauth.should_receive(:base64_url_decode).with(@payload).and_return(result)
414
+ JSON.should_receive(:parse).with(result)
415
+ @oauth.parse_signed_request(@signed_request)
416
+ end
417
+ end
418
+
419
+ describe "if the encoded payload does not match the signature" do
420
+ before :each do
421
+ sig = ""
422
+ @signed_request.should_receive(:split).and_return([sig, ""])
423
+ OpenSSL::HMAC.should_receive(:digest).with(anything, anything, anything).and_return("hi")
424
+ end
425
+
426
+ it "should return nil" do
427
+ @oauth.parse_signed_request(@signed_request).should be_nil
428
+ end
429
+ end
430
+
431
+ describe "run against data" do
432
+ it "should work" do
433
+ @oauth.parse_signed_request(@signed_request).should == @signed_request_result
434
+ end
435
+ end
303
436
  end
304
-
305
- # END CODE THAT NEEDS MOCKING
437
+
306
438
  end # describe
307
439
 
308
440
  end #class