tyler_koala 1.2.0beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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