tyler_koala 1.2.0beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +5 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +185 -0
  5. data/Gemfile +11 -0
  6. data/LICENSE +22 -0
  7. data/Manifest +39 -0
  8. data/Rakefile +16 -0
  9. data/autotest/discover.rb +1 -0
  10. data/koala.gemspec +50 -0
  11. data/lib/koala.rb +119 -0
  12. data/lib/koala/batch_operation.rb +74 -0
  13. data/lib/koala/graph_api.rb +281 -0
  14. data/lib/koala/graph_batch_api.rb +87 -0
  15. data/lib/koala/graph_collection.rb +54 -0
  16. data/lib/koala/http_service.rb +161 -0
  17. data/lib/koala/oauth.rb +181 -0
  18. data/lib/koala/realtime_updates.rb +89 -0
  19. data/lib/koala/rest_api.rb +95 -0
  20. data/lib/koala/test_users.rb +102 -0
  21. data/lib/koala/uploadable_io.rb +180 -0
  22. data/lib/koala/utils.rb +7 -0
  23. data/readme.md +160 -0
  24. data/spec/cases/api_base_spec.rb +101 -0
  25. data/spec/cases/error_spec.rb +30 -0
  26. data/spec/cases/graph_and_rest_api_spec.rb +48 -0
  27. data/spec/cases/graph_api_batch_spec.rb +600 -0
  28. data/spec/cases/graph_api_spec.rb +42 -0
  29. data/spec/cases/http_service_spec.rb +420 -0
  30. data/spec/cases/koala_spec.rb +21 -0
  31. data/spec/cases/oauth_spec.rb +428 -0
  32. data/spec/cases/realtime_updates_spec.rb +198 -0
  33. data/spec/cases/rest_api_spec.rb +41 -0
  34. data/spec/cases/test_users_spec.rb +281 -0
  35. data/spec/cases/uploadable_io_spec.rb +206 -0
  36. data/spec/cases/utils_spec.rb +8 -0
  37. data/spec/fixtures/beach.jpg +0 -0
  38. data/spec/fixtures/cat.m4v +0 -0
  39. data/spec/fixtures/facebook_data.yml +61 -0
  40. data/spec/fixtures/mock_facebook_responses.yml +439 -0
  41. data/spec/spec_helper.rb +43 -0
  42. data/spec/support/graph_api_shared_examples.rb +502 -0
  43. data/spec/support/json_testing_fix.rb +42 -0
  44. data/spec/support/koala_test.rb +163 -0
  45. data/spec/support/mock_http_service.rb +98 -0
  46. data/spec/support/ordered_hash.rb +205 -0
  47. data/spec/support/rest_api_shared_examples.rb +285 -0
  48. data/spec/support/uploadable_io_shared_examples.rb +70 -0
  49. metadata +221 -0
@@ -0,0 +1,42 @@
1
+ # when testing across Ruby versions, we found that JSON string creation inconsistently ordered keys
2
+ # which is a problem because our mock testing service ultimately matches strings to see if requests are mocked
3
+ # this fix solves that problem by ensuring all hashes are created with a consistent key order every time
4
+ module MultiJson
5
+ self.engine = :ok_json
6
+
7
+ class << self
8
+ def encode_with_ordering(object)
9
+ # if it's a hash, recreate it with k/v pairs inserted in sorted-by-key order
10
+ # (for some reason, REE fails if we don't assign the ternary result as a local variable
11
+ # separately from calling encode_original)
12
+ encode_original(sort_object(object))
13
+ end
14
+
15
+ alias_method :encode_original, :encode
16
+ alias_method :encode, :encode_with_ordering
17
+
18
+ def decode_with_ordering(string)
19
+ sort_object(decode_original(string))
20
+ end
21
+
22
+ alias_method :decode_original, :decode
23
+ alias_method :decode, :decode_with_ordering
24
+
25
+ private
26
+
27
+ def sort_object(object)
28
+ if object.is_a?(Hash)
29
+ sort_hash(object)
30
+ elsif object.is_a?(Array)
31
+ object.collect {|item| item.is_a?(Hash) ? sort_hash(item) : item}
32
+ else
33
+ object
34
+ end
35
+ end
36
+
37
+ def sort_hash(unsorted_hash)
38
+ sorted_hash = KoalaTest::OrderedHash.new(sorted_hash)
39
+ unsorted_hash.keys.sort {|a, b| a.to_s <=> b.to_s}.inject(sorted_hash) {|hash, k| hash[k] = unsorted_hash[k]; hash}
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,163 @@
1
+ # small helper method for live testing
2
+ module KoalaTest
3
+
4
+ class << self
5
+ attr_accessor :oauth_token, :app_id, :secret, :app_access_token, :code, :session_key
6
+ attr_accessor :oauth_test_data, :subscription_test_data
7
+ end
8
+
9
+ # Test setup
10
+
11
+ def self.setup_test_environment!
12
+ setup_rspec
13
+
14
+ unless ENV['LIVE']
15
+ # By default the Koala specs are run using stubs for HTTP requests,
16
+ # so they won't fail due to Facebook-imposed rate limits or server timeouts.
17
+ #
18
+ # However as a result they are more brittle since
19
+ # we are not testing the latest responses from the Facebook servers.
20
+ # To be certain all specs pass with the current Facebook services,
21
+ # run LIVE=true bundle exec rake spec.
22
+ Koala.http_service = Koala::MockHTTPService
23
+ KoalaTest.setup_test_data(Koala::MockHTTPService::TEST_DATA)
24
+ else
25
+ # Runs Koala specs through the Facebook servers
26
+ # using data for a real app
27
+ live_data = YAML.load_file(File.join(File.dirname(__FILE__), '../fixtures/facebook_data.yml'))
28
+ KoalaTest.setup_test_data(live_data)
29
+
30
+ # allow live tests with different adapters
31
+ adapter = ENV['ADAPTER'] || "typhoeus"# use Typhoeus by default if available
32
+ begin
33
+ require adapter
34
+ Faraday.default_adapter = adapter.to_sym
35
+ rescue LoadError
36
+ puts "Unable to load adapter #{adapter}, using Net::HTTP."
37
+ end
38
+
39
+ # use a test user unless the developer wants to test against a real profile
40
+ unless token = KoalaTest.oauth_token
41
+ KoalaTest.setup_test_users
42
+ else
43
+ KoalaTest.validate_user_info(token)
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.setup_rspec
49
+ # set up a global before block to set the token for tests
50
+ # set the token up for
51
+ RSpec.configure do |config|
52
+ config.before :each do
53
+ @token = KoalaTest.oauth_token
54
+ Koala::Utils.stub(:deprecate) # never fire deprecation warnings
55
+ end
56
+
57
+ config.after :each do
58
+ # clean up any objects posted to Facebook
59
+ if @temporary_object_id && !KoalaTest.mock_interface?
60
+ api = @api || (@test_users ? @test_users.graph_api : nil)
61
+ raise "Unable to locate API when passed temporary object to delete!" unless api
62
+
63
+ # wait 10ms to allow Facebook to propagate data so we can delete it
64
+ sleep(0.01)
65
+
66
+ # clean up any objects we've posted
67
+ result = (api.delete_object(@temporary_object_id) rescue false)
68
+ # if we errored out or Facebook returned false, track that
69
+ puts "Unable to delete #{@temporary_object_id}: #{result} (probably a photo or video, which can't be deleted through the API)" unless result
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ def self.setup_test_data(data)
76
+ # make data accessible to all our tests
77
+ self.oauth_test_data = data["oauth_test_data"]
78
+ self.subscription_test_data = data["subscription_test_data"]
79
+ self.oauth_token = data["oauth_token"]
80
+ self.app_id = data["oauth_test_data"]["app_id"]
81
+ self.app_access_token = data["oauth_test_data"]["app_access_token"]
82
+ self.secret = data["oauth_test_data"]["secret"]
83
+ self.code = data["oauth_test_data"]["code"]
84
+ self.session_key = data["oauth_test_data"]["session_key"]
85
+ end
86
+
87
+ def self.testing_permissions
88
+ "read_stream, publish_stream, user_photos, user_videos, read_insights"
89
+ end
90
+
91
+ def self.setup_test_users
92
+ # note: we don't have to delete the two test users explicitly, since the test user specs do that for us
93
+ # technically, this is a point of brittleness and would break if the tests were run out of order
94
+ # however, for now we can live with it since it would slow tests way too much to constantly recreate our test users
95
+ print "Setting up test users..."
96
+ @test_user_api = Koala::Facebook::TestUsers.new(:app_id => self.app_id, :secret => self.secret)
97
+
98
+ # create two test users with specific names and befriend them
99
+ @live_testing_user = @test_user_api.create(true, testing_permissions, :name => user1_name)
100
+ @live_testing_friend = @test_user_api.create(true, testing_permissions, :name => user2_name)
101
+ @test_user_api.befriend(@live_testing_user, @live_testing_friend)
102
+ self.oauth_token = @live_testing_user["access_token"]
103
+
104
+ puts "done."
105
+ end
106
+
107
+ def self.validate_user_info(token)
108
+ print "Validating permissions for live testing..."
109
+ # make sure we have the necessary permissions
110
+ api = Koala::Facebook::API.new(token)
111
+ perms = api.fql_query("select #{testing_permissions} from permissions where uid = me()")[0]
112
+ perms.each_pair do |perm, value|
113
+ if value == (perm == "read_insights" ? 1 : 0) # live testing depends on insights calls failing
114
+ puts "failed!\n" # put a new line after the print above
115
+ raise ArgumentError, "Your access token must have the read_stream, publish_stream, and user_photos permissions, and lack read_insights. You have: #{perms.inspect}"
116
+ end
117
+ end
118
+ puts "done!"
119
+ end
120
+
121
+ # Info about the testing environment
122
+ def self.real_user?
123
+ !(mock_interface? || @test_user)
124
+ end
125
+
126
+ def self.test_user?
127
+ !!@test_user_api
128
+ end
129
+
130
+ def self.mock_interface?
131
+ Koala.http_service == Koala::MockHTTPService
132
+ end
133
+
134
+ # Data for testing
135
+ def self.user1
136
+ test_user? ? @live_testing_user["id"] : "koppel"
137
+ end
138
+
139
+ def self.user1_id
140
+ test_user? ? @live_testing_user["id"] : 2905623
141
+ end
142
+
143
+ def self.user1_name
144
+ "Alex"
145
+ end
146
+
147
+ def self.user2
148
+ test_user? ? @live_testing_friend["id"] : "lukeshepard"
149
+ end
150
+
151
+ def self.user2_id
152
+ test_user? ? @live_testing_friend["id"] : 2901279
153
+ end
154
+
155
+ def self.user2_name
156
+ "Luke"
157
+ end
158
+
159
+ def self.page
160
+ "contextoptional"
161
+ end
162
+
163
+ end
@@ -0,0 +1,98 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+
4
+ module Koala
5
+ module MockHTTPService
6
+ include Koala::HTTPService
7
+
8
+ # fix our specs to use ok_json, so we always get the same results from to_json
9
+ MultiJson.engine = :ok_json
10
+
11
+ # Mocks all HTTP requests for with koala_spec_with_mocks.rb
12
+ # Mocked values to be included in TEST_DATA used in specs
13
+ ACCESS_TOKEN = '*'
14
+ APP_ACCESS_TOKEN = "**"
15
+ OAUTH_CODE = 'OAUTHCODE'
16
+
17
+ # Loads testing data
18
+ TEST_DATA = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'fixtures', 'facebook_data.yml'))
19
+ TEST_DATA.merge!('oauth_token' => Koala::MockHTTPService::ACCESS_TOKEN)
20
+ TEST_DATA['oauth_test_data'].merge!('code' => Koala::MockHTTPService::OAUTH_CODE)
21
+
22
+ # Useful in mock_facebook_responses.yml
23
+ OAUTH_DATA = TEST_DATA['oauth_test_data']
24
+ OAUTH_DATA.merge!({
25
+ 'app_access_token' => APP_ACCESS_TOKEN,
26
+ 'session_key' => "session_key",
27
+ 'multiple_session_keys' => ["session_key", "session_key_2"]
28
+ })
29
+ APP_ID = OAUTH_DATA['app_id']
30
+ SECRET = OAUTH_DATA['secret']
31
+ SUBSCRIPTION_DATA = TEST_DATA["subscription_test_data"]
32
+
33
+ # Loads the mock response data via ERB to substitue values for TEST_DATA (see oauth/access_token)
34
+ mock_response_file_path = File.join(File.dirname(__FILE__), '..', 'fixtures', 'mock_facebook_responses.yml')
35
+ RESPONSES = YAML.load(ERB.new(IO.read(mock_response_file_path)).result(binding))
36
+
37
+ def self.make_request(path, args, verb, options = {})
38
+ path = 'root' if path == '' || path == '/'
39
+ verb ||= 'get'
40
+ server = options[:rest_api] ? 'rest_api' : 'graph_api'
41
+ token = args.delete('access_token')
42
+ with_token = (token == ACCESS_TOKEN || token == APP_ACCESS_TOKEN) ? 'with_token' : 'no_token'
43
+
44
+ # Assume format is always JSON
45
+ args.delete('format')
46
+
47
+ # Create a hash key for the arguments
48
+ args = create_params_key(args)
49
+
50
+ begin
51
+ response = RESPONSES[server][path][args][verb][with_token]
52
+
53
+ # Raises an error of with_token/no_token key is missing
54
+ raise NoMethodError unless response
55
+
56
+ # create response class object
57
+ response_object = if response.is_a? String
58
+ Koala::Response.new(200, response, {})
59
+ else
60
+ Koala::Response.new(response["code"] || 200, response["body"] || "", response["headers"] || {})
61
+ end
62
+
63
+ rescue NoMethodError
64
+ # Raises an error message with the place in the data YML
65
+ # to place a mock as well as a URL to request from
66
+ # Facebook's servers for the actual data
67
+ # (Don't forget to replace ACCESS_TOKEN with a real access token)
68
+ data_trace = [server, path, args, verb, with_token] * ': '
69
+
70
+ args = args == 'no_args' ? '' : "#{args}&"
71
+ args += 'format=json'
72
+ args += "&access_token=#{ACCESS_TOKEN}" if with_token
73
+
74
+ raise "Missing a mock response for #{data_trace}\nAPI PATH: #{[path, args].join('?')}"
75
+ end
76
+
77
+ response_object
78
+ end
79
+
80
+ def self.encode_params(*args)
81
+ # use HTTPService's encode_params
82
+ HTTPService.encode_params(*args)
83
+ end
84
+
85
+ protected
86
+
87
+ def self.create_params_key(params_hash)
88
+ if params_hash.empty?
89
+ 'no_args'
90
+ else
91
+ params_hash.sort{ |a,b| a[0].to_s <=> b[0].to_s}.map do |arr|
92
+ arr[1] = '[FILE]' if arr[1].kind_of?(Koala::UploadableIO)
93
+ arr.join('=')
94
+ end.join('&')
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,205 @@
1
+ module KoalaTest
2
+ # directly taken from Rails 3.1's OrderedHash
3
+ # see https://github.com/rails/rails/blob/master/activesupport/lib/active_support/ordered_hash.rb
4
+
5
+ # The order of iteration over hashes in Ruby 1.8 is undefined. For example, you do not know the
6
+ # order in which +keys+ will return keys, or +each+ yield pairs. <tt>ActiveSupport::OrderedHash</tt>
7
+ # implements a hash that preserves insertion order, as in Ruby 1.9:
8
+ #
9
+ # oh = ActiveSupport::OrderedHash.new
10
+ # oh[:a] = 1
11
+ # oh[:b] = 2
12
+ # oh.keys # => [:a, :b], this order is guaranteed
13
+ #
14
+ # <tt>ActiveSupport::OrderedHash</tt> is namespaced to prevent conflicts with other implementations.
15
+ class OrderedHash < ::Hash #:nodoc:
16
+ def to_yaml_type
17
+ "!tag:yaml.org,2002:omap"
18
+ end
19
+
20
+ def encode_with(coder)
21
+ coder.represent_seq '!omap', map { |k,v| { k => v } }
22
+ end
23
+
24
+ def to_yaml(opts = {})
25
+ if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck?
26
+ return super
27
+ end
28
+
29
+ YAML.quick_emit(self, opts) do |out|
30
+ out.seq(taguri) do |seq|
31
+ each do |k, v|
32
+ seq.add(k => v)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def nested_under_indifferent_access
39
+ self
40
+ end
41
+
42
+ # Hash is ordered in Ruby 1.9!
43
+ if RUBY_VERSION < '1.9'
44
+
45
+ # In MRI the Hash class is core and written in C. In particular, methods are
46
+ # programmed with explicit C function calls and polymorphism is not honored.
47
+ #
48
+ # For example, []= is crucial in this implementation to maintain the @keys
49
+ # array but hash.c invokes rb_hash_aset() originally. This prevents method
50
+ # reuse through inheritance and forces us to reimplement stuff.
51
+ #
52
+ # For instance, we cannot use the inherited #merge! because albeit the algorithm
53
+ # itself would work, our []= is not being called at all by the C code.
54
+
55
+ def initialize(*args, &block)
56
+ super
57
+ @keys = []
58
+ end
59
+
60
+ def self.[](*args)
61
+ ordered_hash = new
62
+
63
+ if (args.length == 1 && args.first.is_a?(Array))
64
+ args.first.each do |key_value_pair|
65
+ next unless (key_value_pair.is_a?(Array))
66
+ ordered_hash[key_value_pair[0]] = key_value_pair[1]
67
+ end
68
+
69
+ return ordered_hash
70
+ end
71
+
72
+ unless (args.size % 2 == 0)
73
+ raise ArgumentError.new("odd number of arguments for Hash")
74
+ end
75
+
76
+ args.each_with_index do |val, ind|
77
+ next if (ind % 2 != 0)
78
+ ordered_hash[val] = args[ind + 1]
79
+ end
80
+
81
+ ordered_hash
82
+ end
83
+
84
+ def initialize_copy(other)
85
+ super
86
+ # make a deep copy of keys
87
+ @keys = other.keys
88
+ end
89
+
90
+ def []=(key, value)
91
+ @keys << key unless has_key?(key)
92
+ super
93
+ end
94
+
95
+ def delete(key)
96
+ if has_key? key
97
+ index = @keys.index(key)
98
+ @keys.delete_at index
99
+ end
100
+ super
101
+ end
102
+
103
+ def delete_if
104
+ super
105
+ sync_keys!
106
+ self
107
+ end
108
+
109
+ def reject!
110
+ super
111
+ sync_keys!
112
+ self
113
+ end
114
+
115
+ def reject(&block)
116
+ dup.reject!(&block)
117
+ end
118
+
119
+ def keys
120
+ @keys.dup
121
+ end
122
+
123
+ def values
124
+ @keys.collect { |key| self[key] }
125
+ end
126
+
127
+ def to_hash
128
+ self
129
+ end
130
+
131
+ def to_a
132
+ @keys.map { |key| [ key, self[key] ] }
133
+ end
134
+
135
+ def each_key
136
+ return to_enum(:each_key) unless block_given?
137
+ @keys.each { |key| yield key }
138
+ self
139
+ end
140
+
141
+ def each_value
142
+ return to_enum(:each_value) unless block_given?
143
+ @keys.each { |key| yield self[key]}
144
+ self
145
+ end
146
+
147
+ def each
148
+ return to_enum(:each) unless block_given?
149
+ @keys.each {|key| yield [key, self[key]]}
150
+ self
151
+ end
152
+
153
+ alias_method :each_pair, :each
154
+
155
+ alias_method :select, :find_all
156
+
157
+ def clear
158
+ super
159
+ @keys.clear
160
+ self
161
+ end
162
+
163
+ def shift
164
+ k = @keys.first
165
+ v = delete(k)
166
+ [k, v]
167
+ end
168
+
169
+ def merge!(other_hash)
170
+ if block_given?
171
+ other_hash.each { |k, v| self[k] = key?(k) ? yield(k, self[k], v) : v }
172
+ else
173
+ other_hash.each { |k, v| self[k] = v }
174
+ end
175
+ self
176
+ end
177
+
178
+ alias_method :update, :merge!
179
+
180
+ def merge(other_hash, &block)
181
+ dup.merge!(other_hash, &block)
182
+ end
183
+
184
+ # When replacing with another hash, the initial order of our keys must come from the other hash -ordered or not.
185
+ def replace(other)
186
+ super
187
+ @keys = other.keys
188
+ self
189
+ end
190
+
191
+ def invert
192
+ OrderedHash[self.to_a.map!{|key_value_pair| key_value_pair.reverse}]
193
+ end
194
+
195
+ def inspect
196
+ "#<OrderedHash #{super}>"
197
+ end
198
+
199
+ private
200
+ def sync_keys!
201
+ @keys.delete_if {|k| !has_key?(k)}
202
+ end
203
+ end
204
+ end
205
+ end