koala 1.5.0 → 1.6.0.rc1
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.
- data/.gitignore +2 -1
- data/.travis.yml +4 -1
- data/Gemfile +1 -1
- data/changelog.md +293 -0
- data/koala.gemspec +3 -2
- data/lib/koala.rb +1 -2
- data/lib/koala/api.rb +11 -31
- data/lib/koala/api/batch_operation.rb +1 -1
- data/lib/koala/api/graph_api.rb +132 -62
- data/lib/koala/api/graph_batch_api.rb +28 -38
- data/lib/koala/api/graph_collection.rb +3 -1
- data/lib/koala/api/rest_api.rb +19 -3
- data/lib/koala/errors.rb +86 -0
- data/lib/koala/oauth.rb +21 -21
- data/lib/koala/realtime_updates.rb +42 -21
- data/lib/koala/version.rb +1 -1
- data/readme.md +130 -103
- data/spec/cases/api_spec.rb +3 -3
- data/spec/cases/error_spec.rb +91 -20
- data/spec/cases/graph_api_batch_spec.rb +57 -22
- data/spec/cases/graph_api_spec.rb +68 -0
- data/spec/cases/graph_collection_spec.rb +6 -0
- data/spec/cases/oauth_spec.rb +16 -16
- data/spec/cases/realtime_updates_spec.rb +80 -82
- data/spec/cases/test_users_spec.rb +21 -18
- data/spec/fixtures/mock_facebook_responses.yml +45 -29
- data/spec/spec_helper.rb +6 -6
- data/spec/support/graph_api_shared_examples.rb +13 -13
- data/spec/support/koala_test.rb +13 -13
- data/spec/support/rest_api_shared_examples.rb +3 -3
- metadata +30 -14
- data/CHANGELOG +0 -275
@@ -19,35 +19,23 @@ module Koala
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def graph_call_in_batch(path, args = {}, verb = "get", options = {}, &post_processing)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
22
|
+
# for batch APIs, we queue up the call details (incl. post-processing)
|
23
|
+
batch_calls << BatchOperation.new(
|
24
|
+
:url => path,
|
25
|
+
:args => args,
|
26
|
+
:method => verb,
|
27
|
+
:access_token => options['access_token'] || access_token,
|
28
|
+
:http_options => options,
|
29
|
+
:post_processing => post_processing
|
30
|
+
)
|
31
|
+
nil # batch operations return nothing immediately
|
32
32
|
end
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
# old error format -- see http://developers.facebook.com/blog/post/596/
|
37
|
-
APIError.new({"type" => "Error #{response["error"]}", "message" => response["error_description"]}.merge(response))
|
38
|
-
else
|
39
|
-
check_graph_api_response(response)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
# redefine the graph_call and check_response methods
|
44
|
-
# so we can use this API inside the batch block just like any regular Graph API
|
34
|
+
# redefine the graph_call method so we can use this API inside the batch block
|
35
|
+
# just like any regular Graph API
|
45
36
|
alias_method :graph_call_outside_batch, :graph_call
|
46
37
|
alias_method :graph_call, :graph_call_in_batch
|
47
38
|
|
48
|
-
alias_method :check_graph_api_response, :check_response
|
49
|
-
alias_method :check_response, :check_graph_batch_api_response
|
50
|
-
|
51
39
|
# execute the queued batch calls
|
52
40
|
def execute(http_options = {})
|
53
41
|
return [] unless batch_calls.length > 0
|
@@ -62,7 +50,7 @@ module Koala
|
|
62
50
|
unless response
|
63
51
|
# Facebook sometimes reportedly returns an empty body at times
|
64
52
|
# see https://github.com/arsduo/koala/issues/184
|
65
|
-
raise
|
53
|
+
raise BadFacebookResponse.new(200, '', "Facebook returned an empty body")
|
66
54
|
end
|
67
55
|
|
68
56
|
# map the results with post-processing included
|
@@ -72,13 +60,16 @@ module Koala
|
|
72
60
|
batch_op = batch_calls[index]
|
73
61
|
index += 1
|
74
62
|
|
63
|
+
raw_result = nil
|
75
64
|
if call_result
|
76
|
-
|
77
|
-
|
65
|
+
if ( error = check_response(call_result['code'], call_result['body'].to_s) )
|
66
|
+
raw_result = error
|
67
|
+
else
|
68
|
+
# (see note in regular api method about JSON parsing)
|
69
|
+
body = MultiJson.load("[#{call_result['body'].to_s}]")[0]
|
78
70
|
|
79
|
-
unless call_result["code"].to_i >= 500 || error = check_response(body)
|
80
71
|
# Get the HTTP component they want
|
81
|
-
|
72
|
+
raw_result = case batch_op.http_options[:http_component]
|
82
73
|
when :status
|
83
74
|
call_result["code"].to_i
|
84
75
|
when :headers
|
@@ -87,20 +78,19 @@ module Koala
|
|
87
78
|
else
|
88
79
|
body
|
89
80
|
end
|
90
|
-
|
91
|
-
# process it if we are given a block to process with
|
92
|
-
batch_op.post_processing ? batch_op.post_processing.call(data) : data
|
93
|
-
else
|
94
|
-
error || APIError.new({"type" => "HTTP #{call_result["code"].to_s}", "message" => "Response body: #{body}"})
|
95
81
|
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# turn any results that are pageable into GraphCollections
|
85
|
+
# and pass to post-processing callback if given
|
86
|
+
result = GraphCollection.evaluate(raw_result, @original_api)
|
87
|
+
if batch_op.post_processing
|
88
|
+
batch_op.post_processing.call(result)
|
96
89
|
else
|
97
|
-
|
90
|
+
result
|
98
91
|
end
|
99
92
|
end
|
100
93
|
end
|
101
|
-
|
102
|
-
# turn any results that are pageable into GraphCollections
|
103
|
-
batch_result.inject([]) {|processed_results, raw| processed_results << GraphCollection.evaluate(raw, @original_api)}
|
104
94
|
end
|
105
95
|
|
106
96
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
|
1
3
|
module Koala
|
2
4
|
module Facebook
|
3
5
|
class API
|
@@ -87,7 +89,7 @@ module Koala
|
|
87
89
|
#
|
88
90
|
# @return an array of parameters that can be provided via graph_call(*parsed_params)
|
89
91
|
def self.parse_page_url(url)
|
90
|
-
uri = URI.parse(url)
|
92
|
+
uri = Addressable::URI.parse(url)
|
91
93
|
|
92
94
|
base = uri.path.sub(/^\//, '')
|
93
95
|
params = CGI.parse(uri.query)
|
data/lib/koala/api/rest_api.rb
CHANGED
@@ -21,7 +21,7 @@ module Koala
|
|
21
21
|
#
|
22
22
|
# @return true if successful, false if not. (This call currently doesn't give useful feedback on failure.)
|
23
23
|
def set_app_properties(properties, args = {}, options = {})
|
24
|
-
raise
|
24
|
+
raise AuthenticationError.new(nil, nil, "setAppProperties requires an access token") unless @access_token
|
25
25
|
rest_call("admin.setAppProperties", args.merge(:properties => MultiJson.dump(properties)), options, "post")
|
26
26
|
end
|
27
27
|
|
@@ -44,8 +44,24 @@ module Koala
|
|
44
44
|
|
45
45
|
api("method/#{fb_method}", args.merge('format' => 'json'), verb, options) do |response|
|
46
46
|
# check for REST API-specific errors
|
47
|
-
if response.
|
48
|
-
|
47
|
+
if response.status >= 400
|
48
|
+
begin
|
49
|
+
response_hash = MultiJson.load(response.body)
|
50
|
+
rescue MultiJson::DecodeError
|
51
|
+
response_hash = {}
|
52
|
+
end
|
53
|
+
|
54
|
+
error_info = {
|
55
|
+
'code' => response_hash['error_code'],
|
56
|
+
'error_subcode' => response_hash['error_subcode'],
|
57
|
+
'message' => response_hash['error_msg']
|
58
|
+
}
|
59
|
+
|
60
|
+
if response.status >= 500
|
61
|
+
raise ServerError.new(response.status, response.body, error_info)
|
62
|
+
else
|
63
|
+
raise ClientError.new(response.status, response.body, error_info)
|
64
|
+
end
|
49
65
|
end
|
50
66
|
end
|
51
67
|
end
|
data/lib/koala/errors.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Koala
|
2
|
+
|
3
|
+
class KoalaError < StandardError; end
|
4
|
+
|
5
|
+
module Facebook
|
6
|
+
|
7
|
+
# The OAuth signature is incomplete, invalid, or using an unsupported algorithm
|
8
|
+
class OAuthSignatureError < ::Koala::KoalaError; end
|
9
|
+
|
10
|
+
# Facebook responded with an error to an API request. If the exception contains a nil
|
11
|
+
# http_status, then the error was detected before making a call to Facebook. (e.g. missing access token)
|
12
|
+
class APIError < ::Koala::KoalaError
|
13
|
+
attr_accessor :fb_error_type, :fb_error_code, :fb_error_subcode, :fb_error_message,
|
14
|
+
:http_status, :response_body
|
15
|
+
|
16
|
+
# Create a new API Error
|
17
|
+
#
|
18
|
+
# @param http_status [Integer] The HTTP status code of the response
|
19
|
+
# @param response_body [String] The response body
|
20
|
+
# @param error_info One of the following:
|
21
|
+
# [Hash] The error information extracted from the request
|
22
|
+
# ("type", "code", "error_subcode", "message")
|
23
|
+
# [String] The error description
|
24
|
+
# If error_info is nil or not provided, the method will attempt to extract
|
25
|
+
# the error info from the response_body
|
26
|
+
#
|
27
|
+
# @return the newly created APIError
|
28
|
+
def initialize(http_status, response_body, error_info = nil)
|
29
|
+
if response_body
|
30
|
+
self.response_body = response_body.strip
|
31
|
+
else
|
32
|
+
self.response_body = ''
|
33
|
+
end
|
34
|
+
self.http_status = http_status
|
35
|
+
|
36
|
+
if error_info && error_info.is_a?(String)
|
37
|
+
message = error_info
|
38
|
+
else
|
39
|
+
unless error_info
|
40
|
+
begin
|
41
|
+
error_info = MultiJson.load(response_body)['error'] if response_body
|
42
|
+
rescue
|
43
|
+
end
|
44
|
+
error_info ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
self.fb_error_type = error_info["type"]
|
48
|
+
self.fb_error_code = error_info["code"]
|
49
|
+
self.fb_error_subcode = error_info["error_subcode"]
|
50
|
+
self.fb_error_message = error_info["message"]
|
51
|
+
|
52
|
+
error_array = []
|
53
|
+
%w(type code error_subcode message).each do |key|
|
54
|
+
error_array << "#{key}: #{error_info[key]}" if error_info[key]
|
55
|
+
end
|
56
|
+
|
57
|
+
if error_array.empty?
|
58
|
+
message = self.response_body
|
59
|
+
else
|
60
|
+
message = error_array.join(', ')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
message += " [HTTP #{http_status}]" if http_status
|
64
|
+
|
65
|
+
super(message)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Facebook returned an invalid response body
|
70
|
+
class BadFacebookResponse < APIError; end
|
71
|
+
|
72
|
+
# Facebook responded with an error while attempting to request an access token
|
73
|
+
class OAuthTokenRequestError < APIError; end
|
74
|
+
|
75
|
+
# Any error with a 5xx HTTP status code
|
76
|
+
class ServerError < APIError; end
|
77
|
+
|
78
|
+
# Any error with a 4xx HTTP status code
|
79
|
+
class ClientError < APIError; end
|
80
|
+
|
81
|
+
# All graph API authentication failures.
|
82
|
+
class AuthenticationError < ClientError; end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
data/lib/koala/oauth.rb
CHANGED
@@ -139,7 +139,7 @@ module Koala
|
|
139
139
|
# @param code (see #url_for_access_token)
|
140
140
|
# @param options any additional parameters to send to Facebook when redeeming the token
|
141
141
|
#
|
142
|
-
# @raise Koala::Facebook::
|
142
|
+
# @raise Koala::Facebook::OAuthTokenRequestError if Facebook returns an error response
|
143
143
|
#
|
144
144
|
# @return a hash of the access token info returned by Facebook (token, expiration, etc.)
|
145
145
|
def get_access_token_info(code, options = {})
|
@@ -221,21 +221,21 @@ module Koala
|
|
221
221
|
#
|
222
222
|
# @param input the signed request from Facebook
|
223
223
|
#
|
224
|
-
# @raise
|
224
|
+
# @raise OAuthSignatureError if the signature is incomplete, invalid, or using an unsupported algorithm
|
225
225
|
#
|
226
226
|
# @return a hash of the validated request information
|
227
227
|
def parse_signed_request(input)
|
228
228
|
encoded_sig, encoded_envelope = input.split('.', 2)
|
229
|
-
raise '
|
229
|
+
raise OAuthSignatureError, 'Invalid (incomplete) signature data' unless encoded_sig && encoded_envelope
|
230
230
|
|
231
231
|
signature = base64_url_decode(encoded_sig).unpack("H*").first
|
232
232
|
envelope = MultiJson.load(base64_url_decode(encoded_envelope))
|
233
233
|
|
234
|
-
raise "
|
234
|
+
raise OAuthSignatureError, "Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
|
235
235
|
|
236
236
|
# now see if the signature is valid (digest, key, data)
|
237
237
|
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope)
|
238
|
-
raise '
|
238
|
+
raise OAuthSignatureError, 'Invalid signature' if (signature != hmac)
|
239
239
|
|
240
240
|
envelope
|
241
241
|
end
|
@@ -254,10 +254,7 @@ module Koala
|
|
254
254
|
|
255
255
|
# Facebook returns an empty body in certain error conditions
|
256
256
|
if response == ""
|
257
|
-
raise
|
258
|
-
"type" => "ArgumentError",
|
259
|
-
"message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
|
260
|
-
})
|
257
|
+
raise BadFacebookResponse.new(200, '', "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!")
|
261
258
|
end
|
262
259
|
|
263
260
|
MultiJson.load(response)
|
@@ -282,13 +279,8 @@ module Koala
|
|
282
279
|
|
283
280
|
def get_token_from_server(args, post = false, options = {})
|
284
281
|
# fetch the result from Facebook's servers
|
285
|
-
|
286
|
-
|
287
|
-
# if we have an error, parse the error JSON and raise an error
|
288
|
-
raise APIError.new((MultiJson.load(result)["error"] rescue nil) || {}) if result =~ /error/
|
289
|
-
|
290
|
-
# otherwise, parse the access token
|
291
|
-
parse_access_token(result)
|
282
|
+
response = fetch_token_string(args, post, "access_token", options)
|
283
|
+
parse_access_token(response)
|
292
284
|
end
|
293
285
|
|
294
286
|
def parse_access_token(response_text)
|
@@ -318,9 +310,12 @@ module Koala
|
|
318
310
|
if code = components["code"]
|
319
311
|
begin
|
320
312
|
token_info = get_access_token_info(code, :redirect_uri => '')
|
321
|
-
rescue Koala::Facebook::
|
322
|
-
|
323
|
-
|
313
|
+
rescue Koala::Facebook::OAuthTokenRequestError => err
|
314
|
+
if err.fb_error_type == 'OAuthException' && err.fb_error_message =~ /Code was invalid or expired/
|
315
|
+
return nil
|
316
|
+
else
|
317
|
+
raise
|
318
|
+
end
|
324
319
|
end
|
325
320
|
|
326
321
|
components.merge(token_info) if token_info
|
@@ -331,10 +326,15 @@ module Koala
|
|
331
326
|
end
|
332
327
|
|
333
328
|
def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
|
334
|
-
Koala.make_request("/oauth/#{endpoint}", {
|
329
|
+
response = Koala.make_request("/oauth/#{endpoint}", {
|
335
330
|
:client_id => @app_id,
|
336
331
|
:client_secret => @app_secret
|
337
|
-
}.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options))
|
332
|
+
}.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options))
|
333
|
+
|
334
|
+
raise ServerError.new(response.status, response.body) if response.status >= 500
|
335
|
+
raise OAuthTokenRequestError.new(response.status, response.body) if response.status >= 400
|
336
|
+
|
337
|
+
response.body
|
338
338
|
end
|
339
339
|
|
340
340
|
# base 64
|
@@ -7,19 +7,19 @@ module Koala
|
|
7
7
|
# @note: to subscribe to real-time updates, you must have an application access token
|
8
8
|
# or provide the app secret when initializing your RealtimeUpdates object.
|
9
9
|
|
10
|
-
# The application API interface used to communicate with Facebook.
|
11
|
-
# @return [Koala::Facebook::API]
|
10
|
+
# The application API interface used to communicate with Facebook.
|
11
|
+
# @return [Koala::Facebook::API]
|
12
12
|
attr_reader :api
|
13
13
|
attr_reader :app_id, :app_access_token, :secret
|
14
14
|
|
15
|
-
# Create a new RealtimeUpdates instance.
|
16
|
-
# If you don't have your app's access token, provide the app's secret and
|
15
|
+
# Create a new RealtimeUpdates instance.
|
16
|
+
# If you don't have your app's access token, provide the app's secret and
|
17
17
|
# Koala will make a request to Facebook for the appropriate token.
|
18
|
-
#
|
18
|
+
#
|
19
19
|
# @param options initialization options.
|
20
20
|
# @option options :app_id the application's ID.
|
21
21
|
# @option options :app_access_token an application access token, if known.
|
22
|
-
# @option options :secret the application's secret.
|
22
|
+
# @option options :secret the application's secret.
|
23
23
|
#
|
24
24
|
# @raise ArgumentError if the application ID and one of the app access token or the secret are not provided.
|
25
25
|
def initialize(options = {})
|
@@ -39,7 +39,7 @@ module Koala
|
|
39
39
|
@api = API.new(@app_access_token)
|
40
40
|
end
|
41
41
|
|
42
|
-
# Subscribe to realtime updates for certain fields on a given object (user, page, etc.).
|
42
|
+
# Subscribe to realtime updates for certain fields on a given object (user, page, etc.).
|
43
43
|
# See {http://developers.facebook.com/docs/reference/api/realtime the realtime updates documentation}
|
44
44
|
# for more information on what objects and fields you can register for.
|
45
45
|
#
|
@@ -48,11 +48,11 @@ module Koala
|
|
48
48
|
# @param object a Facebook ID (name or number)
|
49
49
|
# @param fields the fields you want your app to be updated about
|
50
50
|
# @param callback_url the URL Facebook should ping when an update is available
|
51
|
-
# @param verify_token a token included in the verification request, allowing you to ensure the call is genuine
|
51
|
+
# @param verify_token a token included in the verification request, allowing you to ensure the call is genuine
|
52
52
|
# (see the docs for more information)
|
53
53
|
# @param options (see Koala::HTTPService.make_request)
|
54
54
|
#
|
55
|
-
# @
|
55
|
+
# @raise A subclass of Koala::Facebook::APIError if the subscription request failed.
|
56
56
|
def subscribe(object, fields, callback_url, verify_token, options = {})
|
57
57
|
args = {
|
58
58
|
:object => object,
|
@@ -60,23 +60,23 @@ module Koala
|
|
60
60
|
:callback_url => callback_url,
|
61
61
|
}.merge(verify_token ? {:verify_token => verify_token} : {})
|
62
62
|
# a subscription is a success if Facebook returns a 200 (after hitting your server for verification)
|
63
|
-
@api.graph_call(subscription_path, args, 'post', options
|
63
|
+
@api.graph_call(subscription_path, args, 'post', options)
|
64
64
|
end
|
65
65
|
|
66
|
-
# Unsubscribe from updates for a particular object or from updates.
|
66
|
+
# Unsubscribe from updates for a particular object or from updates.
|
67
67
|
#
|
68
|
-
# @param object the object whose subscriptions to delete.
|
68
|
+
# @param object the object whose subscriptions to delete.
|
69
69
|
# If no object is provided, all subscriptions will be removed.
|
70
70
|
# @param options (see Koala::HTTPService.make_request)
|
71
71
|
#
|
72
|
-
# @
|
72
|
+
# @raise A subclass of Koala::Facebook::APIError if the subscription request failed.
|
73
73
|
def unsubscribe(object = nil, options = {})
|
74
|
-
@api.graph_call(subscription_path, object ? {:object => object} : {}, "delete", options
|
74
|
+
@api.graph_call(subscription_path, object ? {:object => object} : {}, "delete", options)
|
75
75
|
end
|
76
76
|
|
77
77
|
# List all active subscriptions for this application.
|
78
|
-
#
|
79
|
-
# @param options (see Koala::HTTPService.make_request)
|
78
|
+
#
|
79
|
+
# @param options (see Koala::HTTPService.make_request)
|
80
80
|
#
|
81
81
|
# @return [Array] a list of active subscriptions
|
82
82
|
def list_subscriptions(options = {})
|
@@ -89,12 +89,12 @@ module Koala
|
|
89
89
|
#
|
90
90
|
# @param params the request parameters sent by Facebook. (You can pass in a Rails params hash.)
|
91
91
|
# @param verify_token the verify token sent in the {#subscribe subscription request}, if you provided one
|
92
|
-
#
|
92
|
+
#
|
93
93
|
# @yield verify_token if you need to compute the verification token
|
94
94
|
# (for instance, if your callback URL includes a record ID, which you look up
|
95
|
-
# and use to calculate a hash), you can pass meet_challenge a block, which
|
95
|
+
# and use to calculate a hash), you can pass meet_challenge a block, which
|
96
96
|
# will receive the verify_token received back from Facebook.
|
97
|
-
#
|
97
|
+
#
|
98
98
|
# @return the challenge string to be sent back to Facebook, or false if the request is invalid.
|
99
99
|
def self.meet_challenge(params, verify_token = nil, &verification_block)
|
100
100
|
if params["hub.mode"] == "subscribe" &&
|
@@ -111,12 +111,33 @@ module Koala
|
|
111
111
|
false
|
112
112
|
end
|
113
113
|
end
|
114
|
-
|
114
|
+
|
115
|
+
# Public: As a security measure, all updates from facebook are signed using
|
116
|
+
# X-Hub-Signature: sha1=XXXX where XXX is the sha1 of the json payload
|
117
|
+
# using your application secret as the key.
|
118
|
+
#
|
119
|
+
# Example:
|
120
|
+
# # in Rails controller
|
121
|
+
# # @oauth being a previously defined Koala::Facebook::OAuth instance
|
122
|
+
# def receive_update
|
123
|
+
# if @oauth.validate_update(request.body, headers)
|
124
|
+
# ...
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
def validate_update(body, headers)
|
128
|
+
if request_signature = headers['X-Hub-Signature'] || headers['HTTP_X_HUB_SIGNATURE'] and
|
129
|
+
signature_parts = request_signature.split("sha1=")
|
130
|
+
request_signature = signature_parts[1]
|
131
|
+
calculated_signature = OpenSSL::HMAC.hexdigest('sha1', @secret, body)
|
132
|
+
calculated_signature == request_signature
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
115
136
|
# The Facebook subscription management URL for your application.
|
116
137
|
def subscription_path
|
117
138
|
@subscription_path ||= "#{@app_id}/subscriptions"
|
118
139
|
end
|
119
|
-
|
140
|
+
|
120
141
|
# @private
|
121
142
|
def graph_api
|
122
143
|
Koala::Utils.deprecate("the TestUsers.graph_api accessor is deprecated and will be removed in a future version; please use .api instead.")
|