koala 0.4 → 3.7.0

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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +32 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +1 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +25 -0
  7. data/ISSUE_TEMPLATE +25 -0
  8. data/LICENSE +22 -0
  9. data/Manifest +32 -5
  10. data/PULL_REQUEST_TEMPLATE +11 -0
  11. data/Rakefile +12 -12
  12. data/changelog.md +781 -0
  13. data/code_of_conduct.md +74 -0
  14. data/koala.gemspec +28 -24
  15. data/lib/koala/api/batch_operation.rb +86 -0
  16. data/lib/koala/api/graph_api_methods.rb +504 -0
  17. data/lib/koala/api/graph_batch_api.rb +167 -0
  18. data/lib/koala/api/graph_collection.rb +129 -0
  19. data/lib/koala/api/graph_error_checker.rb +72 -0
  20. data/lib/koala/api.rb +159 -0
  21. data/lib/koala/configuration.rb +56 -0
  22. data/lib/koala/errors.rb +126 -0
  23. data/lib/koala/http_service/request.rb +133 -0
  24. data/lib/koala/http_service/response.rb +20 -0
  25. data/lib/koala/http_service/uploadable_io.rb +183 -0
  26. data/lib/koala/http_service.rb +108 -0
  27. data/lib/koala/oauth.rb +342 -0
  28. data/lib/koala/realtime_updates.rb +151 -0
  29. data/lib/koala/test_users.rb +189 -0
  30. data/lib/koala/utils.rb +41 -0
  31. data/lib/koala/version.rb +3 -0
  32. data/lib/koala.rb +51 -291
  33. data/readme.md +269 -21
  34. data/spec/cases/api_spec.rb +362 -0
  35. data/spec/cases/configuration_spec.rb +11 -0
  36. data/spec/cases/error_spec.rb +143 -0
  37. data/spec/cases/graph_api_batch_spec.rb +788 -0
  38. data/spec/cases/graph_api_spec.rb +76 -0
  39. data/spec/cases/graph_collection_spec.rb +192 -0
  40. data/spec/cases/graph_error_checker_spec.rb +147 -0
  41. data/spec/cases/http_service/request_spec.rb +250 -0
  42. data/spec/cases/http_service/response_spec.rb +24 -0
  43. data/spec/cases/http_service_spec.rb +280 -0
  44. data/spec/cases/koala_spec.rb +57 -0
  45. data/spec/cases/koala_test_spec.rb +5 -0
  46. data/spec/cases/oauth_spec.rb +647 -0
  47. data/spec/cases/realtime_updates_spec.rb +327 -0
  48. data/spec/cases/test_users_spec.rb +383 -0
  49. data/spec/cases/uploadable_io_spec.rb +266 -0
  50. data/spec/cases/utils_spec.rb +55 -0
  51. data/spec/fixtures/beach.jpg +0 -0
  52. data/spec/fixtures/cat.m4v +0 -0
  53. data/spec/fixtures/facebook_data.yml +63 -0
  54. data/spec/fixtures/mock_facebook_responses.yml +483 -0
  55. data/spec/fixtures/vcr_cassettes/app_test_accounts.yml +97 -0
  56. data/spec/fixtures/vcr_cassettes/friend_list_next_page.yml +121 -0
  57. data/spec/integration/graph_collection_spec.rb +24 -0
  58. data/spec/spec_helper.rb +25 -0
  59. data/spec/support/custom_matchers.rb +28 -0
  60. data/spec/support/graph_api_shared_examples.rb +534 -0
  61. data/spec/support/koala_test.rb +251 -0
  62. data/spec/support/mock_http_service.rb +140 -0
  63. data/spec/support/uploadable_io_shared_examples.rb +70 -0
  64. metadata +206 -62
  65. data/CHANGELOG +0 -24
  66. data/init.rb +0 -2
  67. data/lib/http_services.rb +0 -60
  68. data/test/facebook_data.yml +0 -5
  69. data/test/koala/facebook_no_access_token_tests.rb +0 -119
  70. data/test/koala/facebook_with_access_token_tests.rb +0 -106
  71. data/test/koala_tests.rb +0 -30
@@ -0,0 +1,251 @@
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, :search_time
7
+ attr_accessor :test_user_api
8
+ attr_accessor :vcr_oauth_token
9
+ end
10
+
11
+ # Test setup
12
+
13
+ def self.setup_test_environment!
14
+ setup_rspec
15
+
16
+ if live?
17
+ # Runs Koala specs through the Facebook servers
18
+ # using data for a real app
19
+ live_data = YAML.load_file(File.join(File.dirname(__FILE__), '../fixtures/facebook_data.yml'))
20
+ KoalaTest.setup_test_data(live_data)
21
+
22
+ activate_adapter!
23
+
24
+ Koala.http_service.http_options[:beta] = true if ENV["beta"] || ENV["BETA"]
25
+
26
+ # use a test user unless the developer wants to test against a real profile
27
+ unless token = KoalaTest.oauth_token
28
+ KoalaTest.setup_test_users
29
+ else
30
+ KoalaTest.validate_user_info(token)
31
+ end
32
+ else
33
+ # By default the Koala specs are run using stubs for HTTP requests,
34
+ # so they won't fail due to Facebook-imposed rate limits or server timeouts.
35
+ #
36
+ # However as a result they are more brittle since
37
+ # we are not testing the latest responses from the Facebook servers.
38
+ # To be certain all specs pass with the current Facebook services,
39
+ # run LIVE=true bundle exec rake spec.
40
+ activate_vcr!
41
+ Koala.http_service = Koala::MockHTTPService
42
+ KoalaTest.setup_test_data(Koala::MockHTTPService::TEST_DATA)
43
+ end
44
+ end
45
+
46
+ def self.setup_rspec
47
+ # set up a global before block to set the token for tests
48
+ # set the token up for
49
+ RSpec.configure do |config|
50
+ config.before :each do
51
+ @token = KoalaTest.oauth_token
52
+ allow(Koala::Utils).to receive(:deprecate) # never fire deprecation warnings
53
+ # Clean up Koala config
54
+ Koala.reset_config
55
+ end
56
+
57
+ config.after :each do
58
+ # if we're working with a real user, clean up any objects posted to Facebook
59
+ # no need to do so for test users, since they get deleted at the end
60
+ if @temporary_object_id && KoalaTest.real_user?
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
+
72
+ end
73
+ end
74
+ end
75
+
76
+ # If we're running live tests against the Facebook API, don't use the VCR
77
+ # fixtures. (Another alternative would be to rerecord those fixtures, but we
78
+ # don't necessarily want to do that.
79
+ def self.with_vcr_unless_live(name)
80
+ if live?
81
+ yield
82
+ else
83
+ begin
84
+ # if we're using VCR, we don't want to use the mock service, which was
85
+ # an early implementation of the same type of tool
86
+ old_adapter = Koala.http_service
87
+ Koala.http_service = Koala::HTTPService
88
+ activate_adapter!
89
+ VCR.use_cassette(name) do
90
+ yield
91
+ end
92
+ ensure
93
+ Koala.http_service = old_adapter
94
+ end
95
+ end
96
+ end
97
+
98
+ def self.setup_test_data(data)
99
+ # make data accessible to all our tests
100
+ self.oauth_test_data = data["oauth_test_data"]
101
+ self.subscription_test_data = data["subscription_test_data"]
102
+ self.oauth_token = data["oauth_token"]
103
+ self.app_id = data["oauth_test_data"]["app_id"].to_s
104
+ self.app_access_token = data["oauth_test_data"]["app_access_token"]
105
+ self.secret = data["oauth_test_data"]["secret"]
106
+ self.code = data["oauth_test_data"]["code"]
107
+ self.session_key = data["oauth_test_data"]["session_key"]
108
+
109
+ # fix the search time so it can be used in the mock responses
110
+ self.search_time = data["search_time"] || (Time.now - 3600).to_s
111
+ end
112
+
113
+ def self.testing_permissions
114
+ "read_stream, publish_actions, user_photos, user_videos, read_insights"
115
+ end
116
+
117
+ def self.setup_test_users
118
+ print "Setting up test users..."
119
+ @test_user_api = Koala::Facebook::TestUsers.new(:app_id => self.app_id, :secret => self.secret)
120
+
121
+ RSpec.configure do |config|
122
+ config.before :suite do
123
+ # before each test module, create two test users with specific names and befriend them
124
+ KoalaTest.create_test_users
125
+ end
126
+
127
+ config.after :suite do
128
+ # after each test module, delete the test users to avoid cluttering up the application
129
+ KoalaTest.destroy_test_users
130
+ end
131
+ end
132
+
133
+ puts "done."
134
+ end
135
+
136
+ def self.create_test_users
137
+ begin
138
+ @live_testing_user = @test_user_api.create(true, KoalaTest.testing_permissions, :name => KoalaTest.user1_name)
139
+ @live_testing_friend = @test_user_api.create(true, KoalaTest.testing_permissions, :name => KoalaTest.user2_name)
140
+ @test_user_api.befriend(@live_testing_user, @live_testing_friend)
141
+ self.oauth_token = @live_testing_user["access_token"]
142
+ rescue Exception => e
143
+ Kernel.warn("Problem creating test users! #{e.message}")
144
+ raise
145
+ end
146
+ end
147
+
148
+ def self.destroy_test_users
149
+ [@live_testing_user, @live_testing_friend].each do |u|
150
+ puts "Unable to delete test user #{u.inspect}" if u && !(@test_user_api.delete(u) rescue false)
151
+ end
152
+ end
153
+
154
+ def self.validate_user_info(token)
155
+ print "Validating permissions for live testing..."
156
+ # make sure we have the necessary permissions
157
+ api = Koala::Facebook::API.new(token)
158
+ perms = api.get_connect("me", "permissions")["data"]
159
+
160
+ # live testing depends on insights calls failing
161
+ if perms.keys.include?("read_insights") || (perms.keys & testing_permissions) != testing_permissions
162
+ puts "failed!\n" # put a new line after the print above
163
+ raise ArgumentError, "Your access token must have #{testing_permissions.join(", ")}, and lack read_insights. You have: #{perms.inspect}"
164
+ end
165
+ puts "done!"
166
+ end
167
+
168
+ # Info about the testing environment
169
+ def self.real_user?
170
+ !(mock_interface? || @test_user_api)
171
+ end
172
+
173
+ def self.test_user?
174
+ !!@test_user_api
175
+ end
176
+
177
+ def self.mock_interface?
178
+ Koala.http_service == Koala::MockHTTPService
179
+ end
180
+
181
+ # Data for testing
182
+ def self.user1
183
+ # user ID, either numeric or username
184
+ test_user? ? @live_testing_user["id"] : "barackobama"
185
+ end
186
+
187
+ def self.user1_id
188
+ # numerical ID, used for FQL
189
+ # (otherwise the two IDs are interchangeable)
190
+ test_user? ? @live_testing_user["id"] : 2905623
191
+ end
192
+
193
+ def self.user1_name
194
+ "Alex"
195
+ end
196
+
197
+ def self.user2
198
+ # see notes for user1
199
+ test_user? ? @live_testing_friend["id"] : "koppel"
200
+ end
201
+
202
+ def self.user2_id
203
+ # see notes for user1
204
+ test_user? ? @live_testing_friend["id"] : 2901279
205
+ end
206
+
207
+ def self.user2_name
208
+ "Luke"
209
+ end
210
+
211
+ def self.page
212
+ "facebook"
213
+ end
214
+
215
+ def self.app_properties
216
+ mock_interface? ? {"desktop" => 0} : {"description" => "A test framework for Koala and its users. (#{rand(10000).to_i})"}
217
+ end
218
+
219
+ def self.live?
220
+ ENV['LIVE']
221
+ end
222
+
223
+ def self.activate_vcr!
224
+ require 'vcr'
225
+
226
+ VCR.configure do |c|
227
+ c.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
228
+ c.hook_into :webmock # or :fakeweb
229
+ c.ignore_hosts 'codeclimate.com' # Allow test coverage to be reported
230
+ end
231
+ end
232
+
233
+ def self.activate_adapter!
234
+ unless @adapter_activation_attempted
235
+ # allow live tests with different adapters
236
+ adapter = ENV['ADAPTER'] || "typhoeus" # use Typhoeus by default if available
237
+ begin
238
+ # JRuby doesn't support typhoeus on Travis
239
+ unless defined? JRUBY_VERSION
240
+ require adapter
241
+ require "faraday/#{adapter}"
242
+ Faraday.default_adapter = adapter.to_sym
243
+ end
244
+ rescue => e
245
+ puts "Unable to load adapter #{adapter}, using Net::HTTP. #{e.class} #{e.message}"
246
+ ensure
247
+ @adapter_activation_attempted = true
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,140 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+
4
+ module Koala
5
+ module MockHTTPService
6
+ include Koala::HTTPService
7
+
8
+ # Mocks all HTTP requests for with koala_spec_with_mocks.rb
9
+ # Mocked values to be included in TEST_DATA used in specs
10
+ ACCESS_TOKEN = '*'
11
+ APP_ACCESS_TOKEN = "********************"
12
+ OAUTH_CODE = 'OAUTHCODE'
13
+
14
+ # Loads testing data
15
+ TEST_DATA = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'fixtures', 'facebook_data.yml'))
16
+ TEST_DATA.merge!('oauth_token' => Koala::MockHTTPService::ACCESS_TOKEN)
17
+ TEST_DATA['oauth_test_data'].merge!('code' => Koala::MockHTTPService::OAUTH_CODE)
18
+ TEST_DATA['search_time'] = (Time.now - 3600).to_s
19
+
20
+ # Useful in mock_facebook_responses.yml
21
+ OAUTH_DATA = TEST_DATA['oauth_test_data']
22
+ OAUTH_DATA.merge!({
23
+ 'app_access_token' => APP_ACCESS_TOKEN,
24
+ 'session_key' => "session_key",
25
+ 'multiple_session_keys' => ["session_key", "session_key_2"]
26
+ })
27
+ APP_ID = OAUTH_DATA['app_id']
28
+ SECRET = OAUTH_DATA['secret']
29
+ SUBSCRIPTION_DATA = TEST_DATA["subscription_test_data"]
30
+
31
+ # Loads the mock response data via ERB to substitue values for TEST_DATA (see oauth/access_token)
32
+ mock_response_file_path = File.join(File.dirname(__FILE__), '..', 'fixtures', 'mock_facebook_responses.yml')
33
+ RESPONSES = YAML.safe_load(ERB.new(IO.read(mock_response_file_path)).result(binding), [], [], true)
34
+
35
+ def self.make_request(request)
36
+ if response = match_response(request.raw_path, request.raw_args, request.raw_verb, request.raw_options)
37
+ # create response class object
38
+ response_object = if response.is_a? String
39
+ Koala::HTTPService::Response.new(200, response, {})
40
+ else
41
+ Koala::HTTPService::Response.new(response["code"] || 200, response["body"] || "", response["headers"] || {})
42
+ end
43
+ else
44
+ # Raises an error message with the place in the data YML
45
+ # to place a mock as well as a URL to request from
46
+ # Facebook's servers for the actual data
47
+ # (Don't forget to replace ACCESS_TOKEN with a real access token)
48
+ data_trace = [request.raw_path, request.raw_args, request.raw_verb, request.raw_options] * ': '
49
+
50
+ args = request.raw_args == 'no_args' ? '' : "#{request.raw_args}&"
51
+ args += 'format=json'
52
+
53
+ raise "Missing a mock response for #{data_trace}\nAPI PATH: #{[request.path, request.raw_args].join('?')}"
54
+ end
55
+
56
+ response_object
57
+ end
58
+
59
+ def self.encode_params(*args)
60
+ # use HTTPService's encode_params
61
+ HTTPService.encode_params(*args)
62
+ end
63
+
64
+ protected
65
+
66
+ # For a given query, see if our mock responses YAML has a resopnse for it.
67
+ def self.match_response(path, args, verb, options = {})
68
+ server = 'graph_api'
69
+ path = 'root' if path == '' || path == '/'
70
+ verb = (verb || 'get').to_s
71
+ token = args.delete('access_token')
72
+ with_token = (token == ACCESS_TOKEN || token == APP_ACCESS_TOKEN) ? 'with_token' : 'no_token'
73
+
74
+ if endpoint = RESPONSES[server][path]
75
+ # see if we have a match for the arguments
76
+ if arg_match = endpoint.find {|query, v| decode_query(query) == massage_args(args)}
77
+ # we have a match for the server/path/arguments
78
+ # so if it's the right verb and authentication, we're good
79
+ # arg_match will be [query, hash_response]
80
+ arg_match.last[verb][with_token]
81
+ end
82
+ end
83
+ end
84
+
85
+ # Since we're comparing the arguments with data in a yaml file, we need to
86
+ # massage them slightly to get to the format we expect.
87
+ def self.massage_args(arguments)
88
+ args = arguments.inject({}) do |hash, (k, v)|
89
+ # ensure our args are all stringified
90
+ value = if v.is_a?(String)
91
+ should_json_decode?(v) ? JSON.parse(v) : v
92
+ elsif v.is_a?(Koala::HTTPService::UploadableIO)
93
+ # obviously there are no files in the yaml
94
+ "[FILE]"
95
+ else
96
+ v
97
+ end
98
+ # make sure all keys are strings
99
+ hash.merge(k.to_s => value)
100
+ end
101
+
102
+ # Assume format is always JSON
103
+ args.delete('format')
104
+
105
+ # if there are no args, return the special keyword no_args
106
+ args.empty? ? "no_args" : args
107
+ end
108
+
109
+ # Facebook sometimes requires us to encode JSON values in an HTTP query
110
+ # param. This complicates test matches, since we get into JSON-encoding
111
+ # issues (what order keys are written into). To avoid string comparisons
112
+ # and the hacks required to make it work, we decode the query into a
113
+ # Ruby object.
114
+ def self.decode_query(string)
115
+ if string == "no_args"
116
+ string
117
+ else
118
+ # we can't use Faraday's decode_query because that CGI-unencodes, which
119
+ # will remove +'s in restriction strings
120
+ string.split("&").reduce({}) do |hash, component|
121
+ k, v = component.split("=", 2) # we only care about the first =
122
+ value = should_json_decode?(v) ? JSON.parse(v) : v.to_s rescue v.to_s
123
+ # some special-casing, unfortunate but acceptable in this testing
124
+ # environment
125
+ value = nil if value.empty?
126
+ value = true if value == "true"
127
+ value = false if value == "false"
128
+ hash.merge(k => value)
129
+ end
130
+ end
131
+ end
132
+
133
+ # We want to decode JSON because it may not be encoded in the same order
134
+ # all the time -- different Rubies may create a strings with equivalent
135
+ # content but different order. We want to compare the objects.
136
+ def self.should_json_decode?(v)
137
+ v.match(/^[\[\{]/)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,70 @@
1
+ shared_examples_for "MIME::Types can't return results" do
2
+ {
3
+ "jpg" => "image/jpeg",
4
+ "jpeg" => "image/jpeg",
5
+ "png" => "image/png",
6
+ "gif" => "image/gif"
7
+ }.each_pair do |extension, mime_type|
8
+ it "should properly get content types for #{extension} using basic analysis" do
9
+ path = "filename.#{extension}"
10
+ if @koala_io_params[0].is_a?(File)
11
+ allow(@koala_io_params[0]).to receive(:path).and_return(path)
12
+ else
13
+ @koala_io_params[0] = path
14
+ end
15
+ expect(Koala::HTTPService::UploadableIO.new(*@koala_io_params).content_type).to eq(mime_type)
16
+ end
17
+
18
+ it "should get content types for #{extension} using basic analysis with file names with more than one dot" do
19
+ path = "file.name.#{extension}"
20
+ if @koala_io_params[0].is_a?(File)
21
+ allow(@koala_io_params[0]).to receive(:path).and_return(path)
22
+ else
23
+ @koala_io_params[0] = path
24
+ end
25
+ expect(Koala::HTTPService::UploadableIO.new(*@koala_io_params).content_type).to eq(mime_type)
26
+ end
27
+ end
28
+
29
+ describe "if the MIME type can't be determined" do
30
+ before :each do
31
+ path = "badfile.badextension"
32
+ if @koala_io_params[0].is_a?(File)
33
+ allow(@koala_io_params[0]).to receive(:path).and_return(path)
34
+ else
35
+ @koala_io_params[0] = path
36
+ end
37
+ end
38
+
39
+ it "should throw an exception" do
40
+ expect { Koala::HTTPService::UploadableIO.new(*@koala_io_params) }.to raise_exception(Koala::KoalaError)
41
+ end
42
+ end
43
+ end
44
+
45
+ shared_examples_for "determining a mime type" do
46
+ describe "if MIME::Types is available" do
47
+ it "should return an UploadIO with MIME::Types-determined type if the type exists" do
48
+ type_result = ["type"]
49
+ allow(Koala::MIME::Types).to receive(:type_for).and_return(type_result)
50
+ expect(Koala::HTTPService::UploadableIO.new(*@koala_io_params).content_type).to eq(type_result.first)
51
+ end
52
+ end
53
+
54
+ describe "if MIME::Types is unavailable" do
55
+ before :each do
56
+ # fake that MIME::Types doesn't exist
57
+ allow(Koala::MIME::Types).to receive(:type_for).and_raise(NameError)
58
+ end
59
+ it_should_behave_like "MIME::Types can't return results"
60
+ end
61
+
62
+ describe "if MIME::Types can't find the result" do
63
+ before :each do
64
+ # fake that MIME::Types doesn't exist
65
+ allow(Koala::MIME::Types).to receive(:type_for).and_return([])
66
+ end
67
+
68
+ it_should_behave_like "MIME::Types can't return results"
69
+ end
70
+ end