bearcat 1.4.13 → 1.5.0.beta1
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.
- checksums.yaml +4 -4
- data/bearcat.gemspec +9 -3
- data/lib/bearcat/client/account_reports.rb +6 -14
- data/lib/bearcat/client/accounts.rb +20 -56
- data/lib/bearcat/client/analytics.rb +4 -7
- data/lib/bearcat/client/assignment_groups.rb +7 -19
- data/lib/bearcat/client/assignments.rb +15 -39
- data/lib/bearcat/client/blueprint_courses.rb +14 -20
- data/lib/bearcat/client/calendar_events.rb +9 -17
- data/lib/bearcat/client/canvas_files.rb +0 -2
- data/lib/bearcat/client/conferences.rb +3 -8
- data/lib/bearcat/client/conversations.rb +3 -8
- data/lib/bearcat/client/courses.rb +15 -45
- data/lib/bearcat/client/custom_gradebook_columns.rb +11 -25
- data/lib/bearcat/client/discussions.rb +11 -37
- data/lib/bearcat/client/enrollments.rb +9 -29
- data/lib/bearcat/client/external_tools.rb +9 -36
- data/lib/bearcat/client/file_helper.rb +2 -2
- data/lib/bearcat/client/files.rb +2 -4
- data/lib/bearcat/client/folders.rb +13 -59
- data/lib/bearcat/client/group_categories.rb +8 -17
- data/lib/bearcat/client/group_memberships.rb +6 -14
- data/lib/bearcat/client/groups.rb +9 -24
- data/lib/bearcat/client/module_items.rb +9 -17
- data/lib/bearcat/client/modules.rb +10 -20
- data/lib/bearcat/client/outcome_groups.rb +2 -4
- data/lib/bearcat/client/outcomes.rb +4 -7
- data/lib/bearcat/client/pages.rb +8 -23
- data/lib/bearcat/client/progresses.rb +2 -3
- data/lib/bearcat/client/quizzes.rb +15 -35
- data/lib/bearcat/client/reports.rb +8 -19
- data/lib/bearcat/client/roles.rb +1 -1
- data/lib/bearcat/client/rubric.rb +8 -20
- data/lib/bearcat/client/rubric_assessment.rb +5 -11
- data/lib/bearcat/client/rubric_association.rb +6 -12
- data/lib/bearcat/client/search.rb +2 -4
- data/lib/bearcat/client/sections.rb +11 -26
- data/lib/bearcat/client/submissions.rb +13 -36
- data/lib/bearcat/client/tabs.rb +4 -7
- data/lib/bearcat/client/users.rb +17 -44
- data/lib/bearcat/client.rb +72 -44
- data/lib/bearcat/client_module.rb +101 -0
- data/lib/bearcat/rate_limiting/redis_script.rb +164 -0
- data/lib/bearcat/rate_limiting.rb +70 -0
- data/lib/bearcat/spec_helpers.rb +62 -31
- data/lib/bearcat/version.rb +1 -1
- data/lib/bearcat.rb +8 -21
- data/lib/catalogcat/api_array.rb +18 -0
- data/lib/catalogcat/client/catalogs.rb +21 -0
- data/lib/catalogcat/client/certificates.rb +9 -0
- data/lib/catalogcat/client/courses.rb +25 -0
- data/lib/catalogcat/client/orders.rb +9 -0
- data/lib/catalogcat/client.rb +39 -0
- data/lib/catalogcat/version.rb +3 -0
- data/lib/catalogcat.rb +14 -0
- data/spec/bearcat/client/canvas_files_spec.rb +1 -2
- data/spec/bearcat/client/content_migrations_spec.rb +1 -1
- data/spec/bearcat/client/courses_spec.rb +2 -4
- data/spec/bearcat/{group_memberships_spec.rb → client/group_memberships_spec.rb} +0 -0
- data/spec/bearcat/client_spec.rb +1 -4
- data/spec/bearcat/rate_limiting_spec.rb +62 -0
- data/spec/bearcat/stub_bearcat_spec.rb +15 -0
- data/spec/helper.rb +1 -0
- metadata +195 -180
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require_relative 'rate_limiting/redis_script'
|
3
|
+
|
4
|
+
module Bearcat
|
5
|
+
module RateLimiting
|
6
|
+
LEAK_RATE = 10
|
7
|
+
GUESS_COST = 60
|
8
|
+
MAXIMUM = 600
|
9
|
+
|
10
|
+
class RateLimiter
|
11
|
+
attr_reader :namespace
|
12
|
+
|
13
|
+
def initialize(namespace: "bearcat", **kwargs)
|
14
|
+
@namespace = namespace
|
15
|
+
@params = kwargs
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply(key, guess: GUESS_COST, on_sleep: nil, max_sleep: 15)
|
19
|
+
current = increment("#{key}", 0, guess)
|
20
|
+
cost = guess
|
21
|
+
remaining = MAXIMUM - current[:count]
|
22
|
+
|
23
|
+
if remaining < 0
|
24
|
+
tts = -remaining / LEAK_RATE
|
25
|
+
tts = [tts, max_sleep].min if max_sleep
|
26
|
+
on_sleep.call(tts) if on_sleep
|
27
|
+
sleep tts
|
28
|
+
end
|
29
|
+
|
30
|
+
cost = yield
|
31
|
+
ensure
|
32
|
+
increment(key, (cost || 0), -guess)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class RedisLimiter < RateLimiter
|
37
|
+
INCR = RedisScript.new(Pathname.new(__FILE__) + "../rate_limiting/increment_bucket.lua")
|
38
|
+
|
39
|
+
def increment(key, amount, pending_amount = 0)
|
40
|
+
ts = Time.now.to_f
|
41
|
+
|
42
|
+
redis do |r|
|
43
|
+
actual, timestamp = INCR.call(r, ["#{key}|reported"], [amount, ts, LEAK_RATE, MAXIMUM + 300])
|
44
|
+
{
|
45
|
+
count: actual.to_f,
|
46
|
+
timestamp: timestamp.to_f,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def checkin_known(key, remaining_time)
|
52
|
+
redis do |r|
|
53
|
+
r.mapped_hmset("#{key}|reported", {
|
54
|
+
count: MAXIMUM + 200 - remaining_time,
|
55
|
+
timestamp: Time.now.to_f,
|
56
|
+
})
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def redis(&blk)
|
61
|
+
# TODO Add bearcat-native ConnectionPool instead of relying on application or Sidekiq to do so
|
62
|
+
if @params[:redis]
|
63
|
+
@params[:redis].call(blk)
|
64
|
+
else
|
65
|
+
::Sidekiq.redis(&blk)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/bearcat/spec_helpers.rb
CHANGED
@@ -16,36 +16,10 @@ module Bearcat::SpecHelpers
|
|
16
16
|
# Accepts optional keyword parameters to interpolate specific values into the URL.
|
17
17
|
# Returns a mostly-normal Webmock stub (:to_return has been overridden to allow :body to be set to a Hash)
|
18
18
|
def stub_bearcat(endpoint, prefix: nil, method: :auto, **kwargs)
|
19
|
-
|
20
|
-
when Symbol
|
21
|
-
ruby_method = Bearcat::Client.instance_method(endpoint)
|
22
|
-
match = SOURCE_REGEX.match(ruby_method.source)
|
23
|
-
bits = []
|
24
|
-
url = match[:url].gsub(/\/$/, '')
|
25
|
-
lend = 0
|
26
|
-
url.scan(/#\{(?<key>.*?)\}/) do |key|
|
27
|
-
m = Regexp.last_match
|
28
|
-
between = url[lend..m.begin(0)-1]
|
29
|
-
bits << between if between.present? && (m.begin(0) > lend)
|
30
|
-
lend = m.end(0)
|
31
|
-
bits << (kwargs[m[:key].to_sym] || /\w+/)
|
32
|
-
end
|
33
|
-
between = url[lend..-1]
|
34
|
-
bits << between if between.present?
|
35
|
-
|
36
|
-
bits.map do |bit|
|
37
|
-
next bit.source if bit.is_a?(Regexp)
|
38
|
-
bit = bit.canvas_id if bit.respond_to?(:canvas_id)
|
39
|
-
Regexp.escape(bit.to_s)
|
40
|
-
end.join
|
41
|
-
when String
|
42
|
-
Regexp.escape(endpoint)
|
43
|
-
when Regexp
|
44
|
-
endpoint.source
|
45
|
-
end
|
19
|
+
cfg = _bearcat_resolve_config(endpoint, method: method)
|
46
20
|
|
47
|
-
url = Regexp.escape(
|
48
|
-
stub = stub_request(
|
21
|
+
url = Regexp.escape(_bearcat_resolve_prefix(prefix)) + cfg[:url]
|
22
|
+
stub = stub_request(cfg[:method], Regexp.new(url))
|
49
23
|
|
50
24
|
# Override the to_return method to accept a Hash as body:
|
51
25
|
stub.define_singleton_method(:to_return, ->(*resps, &blk) {
|
@@ -72,7 +46,64 @@ module Bearcat::SpecHelpers
|
|
72
46
|
|
73
47
|
private
|
74
48
|
|
75
|
-
def
|
49
|
+
def _bearcat_resolve_config(endpoint, url_context, method: :auto)
|
50
|
+
case endpoint
|
51
|
+
when Symbol
|
52
|
+
if (cfg = Bearcat::Client.registered_endpoints[endpoint]).present?
|
53
|
+
bits = []
|
54
|
+
url = cfg[:url]
|
55
|
+
lend = 0
|
56
|
+
url.scan(/:(?<key>\w+)/) do |key|
|
57
|
+
m = Regexp.last_match
|
58
|
+
between = url[lend..m.begin(0)-1]
|
59
|
+
bits << between if between.present? && (m.begin(0) > lend)
|
60
|
+
lend = m.end(0)
|
61
|
+
bits << (url_context[m[:key].to_sym] || /\w+/)
|
62
|
+
end
|
63
|
+
between = url[lend..-1]
|
64
|
+
bits << between if between.present?
|
65
|
+
|
66
|
+
url = bits.map do |bit|
|
67
|
+
next bit.source if bit.is_a?(Regexp)
|
68
|
+
bit = bit.canvas_id if bit.respond_to?(:canvas_id)
|
69
|
+
Regexp.escape(bit.to_s)
|
70
|
+
end.join
|
71
|
+
|
72
|
+
{ method: method == :auto ? cfg[:method] : method, url: url }
|
73
|
+
else
|
74
|
+
ruby_method = Bearcat::Client.instance_method(endpoint)
|
75
|
+
match = SOURCE_REGEX.match(ruby_method.source)
|
76
|
+
bits = []
|
77
|
+
url = match[:url].gsub(/\/$/, '')
|
78
|
+
lend = 0
|
79
|
+
url.scan(/#\{(?<key>.*?)\}/) do |key|
|
80
|
+
m = Regexp.last_match
|
81
|
+
between = url[lend..m.begin(0)-1]
|
82
|
+
bits << between if between.present? && (m.begin(0) > lend)
|
83
|
+
lend = m.end(0)
|
84
|
+
bits << (url_context[m[:key].to_sym] || /\w+/)
|
85
|
+
end
|
86
|
+
between = url[lend..-1]
|
87
|
+
bits << between if between.present?
|
88
|
+
|
89
|
+
url = bits.map do |bit|
|
90
|
+
next bit.source if bit.is_a?(Regexp)
|
91
|
+
bit = bit.canvas_id if bit.respond_to?(:canvas_id)
|
92
|
+
Regexp.escape(bit.to_s)
|
93
|
+
end.join
|
94
|
+
|
95
|
+
{ method: method == :auto ? (match ? match[:method].to_sym : :get) : method, url: url }
|
96
|
+
end
|
97
|
+
when String
|
98
|
+
raise "Cannot use method :auto when passing string endpoint" if method == :auto
|
99
|
+
{ method: method, url: Regexp.escape(endpoint) }
|
100
|
+
when Regexp
|
101
|
+
raise "Cannot use method :auto when passing regex endpoint" if method == :auto
|
102
|
+
{ method: method, url: Regexp.escape(endpoint) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def _bearcat_resolve_prefix(prefix)
|
76
107
|
if prefix == true
|
77
108
|
prefix = canvas_api_client if defined? canvas_api_client
|
78
109
|
prefix = canvas_sync_client if defined? canvas_sync_client
|
@@ -90,4 +121,4 @@ module Bearcat::SpecHelpers
|
|
90
121
|
prefix
|
91
122
|
end
|
92
123
|
end
|
93
|
-
end
|
124
|
+
end
|
data/lib/bearcat/version.rb
CHANGED
data/lib/bearcat.rb
CHANGED
@@ -4,24 +4,23 @@ require 'bearcat/client'
|
|
4
4
|
module Bearcat
|
5
5
|
class << self
|
6
6
|
require 'logger'
|
7
|
-
attr_writer :
|
8
|
-
:min_sleep_seconds, :logger, :master_rate_limit, :
|
9
|
-
:rate_limit_threshold
|
7
|
+
attr_writer :rate_limit_min, :rate_limits, :max_sleep_seconds,
|
8
|
+
:min_sleep_seconds, :logger, :master_rate_limit, :rate_limiter
|
10
9
|
|
11
10
|
def configure
|
12
11
|
yield self if block_given?
|
13
12
|
end
|
14
13
|
|
15
|
-
def
|
16
|
-
@
|
14
|
+
def rate_limiter
|
15
|
+
@rate_limiter
|
17
16
|
end
|
18
17
|
|
19
|
-
def
|
20
|
-
@
|
18
|
+
def rate_limit_min
|
19
|
+
@rate_limit_min ||= 175
|
21
20
|
end
|
22
21
|
|
23
|
-
def
|
24
|
-
@
|
22
|
+
def min_sleep_seconds
|
23
|
+
@min_sleep_seconds ||= 5
|
25
24
|
end
|
26
25
|
|
27
26
|
def max_sleep_seconds
|
@@ -32,23 +31,11 @@ module Bearcat
|
|
32
31
|
@master_rate_limit ||= false
|
33
32
|
end
|
34
33
|
|
35
|
-
def master_mutex
|
36
|
-
@master_mutex ||= Mutex.new
|
37
|
-
end
|
38
|
-
|
39
|
-
def rate_limit_threshold
|
40
|
-
@rate_limit_threshold ||= 125
|
41
|
-
end
|
42
|
-
|
43
34
|
def logger
|
44
35
|
return @logger if defined? @logger
|
45
36
|
@logger = Logger.new(STDOUT)
|
46
37
|
@logger.level = Logger::DEBUG
|
47
38
|
@logger
|
48
39
|
end
|
49
|
-
|
50
|
-
def min_sleep_seconds
|
51
|
-
@min_sleep_seconds ||= 5
|
52
|
-
end
|
53
40
|
end
|
54
41
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'bearcat/api_array'
|
2
|
+
|
3
|
+
module Catalogcat
|
4
|
+
class ApiArray < Bearcat::ApiArray
|
5
|
+
def self.array_key(response)
|
6
|
+
key = nil
|
7
|
+
if response.env[:method] == :get
|
8
|
+
path = response.env[:url].path
|
9
|
+
key = 'courses' if path =~ %r{.*/courses}
|
10
|
+
key = 'course' if path =~ %r{.*/courses/[0-9]*}
|
11
|
+
key = 'catalogs' if path =~ %r{.*/catalogs}
|
12
|
+
key = 'enrollments' if path =~ %r{.*/enrollments}
|
13
|
+
key = 'order' if path =~ %r{.*/order/[0-9]*}
|
14
|
+
end
|
15
|
+
key.present? ? key : super(response)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Catalogcat
|
2
|
+
class Client < Footrest::Client
|
3
|
+
module Catalogs
|
4
|
+
def list_catalogs(params = {})
|
5
|
+
get('/api/v1/catalogs', params)
|
6
|
+
end
|
7
|
+
|
8
|
+
def applicants(params = {})
|
9
|
+
get('/api/v1/applicants', params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def completed_certificates(params = {})
|
13
|
+
get('/api/v1/completed_certificates', params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_order(id, params = {})
|
17
|
+
get("/api/v1/orders/#{id}", params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Catalogcat
|
2
|
+
class Client < Footrest::Client
|
3
|
+
module Courses
|
4
|
+
def list_courses(page = 1, params = {})
|
5
|
+
get("/api/v1/courses?page=#{page}&per_page=100", params)
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_course(params = {})
|
9
|
+
post('/api/v1/courses', params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def update_course(id, params = {})
|
13
|
+
put("/api/v1/courses/#{id}", params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_course(id, params = {})
|
17
|
+
get("/api/v1/courses/#{id}", params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete_course(course)
|
21
|
+
delete("/api/v1/courses/#{course}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'footrest/client'
|
2
|
+
|
3
|
+
module Catalogcat
|
4
|
+
class Client < Footrest::Client
|
5
|
+
require 'catalogcat/api_array' # monkey patch
|
6
|
+
|
7
|
+
Dir[File.join(__dir__, 'client', '*.rb')].each do |mod|
|
8
|
+
mname = File.basename(mod, '.*').camelize
|
9
|
+
require mod
|
10
|
+
include "Catalogcat::Client::#{mname}".constantize
|
11
|
+
end
|
12
|
+
|
13
|
+
# Override Footrest connection to use Token authorization
|
14
|
+
# rubocop:disable Naming/AccessorMethodName
|
15
|
+
def set_connection(config)
|
16
|
+
super
|
17
|
+
connection.builder.insert(Footrest::RaiseFootrestErrors, ExtendedRaiseFootrestErrors)
|
18
|
+
connection.builder.delete(Footrest::RaiseFootrestErrors)
|
19
|
+
connection.headers[:authorization].sub! 'Bearer', 'Token'
|
20
|
+
end
|
21
|
+
# rubocop:enable Naming/AccessorMethodName
|
22
|
+
|
23
|
+
# Override Footrest request for ApiArray support
|
24
|
+
def request(method, &block)
|
25
|
+
response = connection.send(method, &block)
|
26
|
+
raise Footrest::HttpError::ErrorBase, response if response.status >= 400
|
27
|
+
|
28
|
+
Catalogcat::ApiArray.process_response(response, self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class ExtendedRaiseFootrestErrors < Footrest::RaiseFootrestErrors
|
33
|
+
def on_complete(response)
|
34
|
+
super
|
35
|
+
key = response[:status].to_i
|
36
|
+
raise ERROR_MAP[key.floor(-2)], response if key >= 400
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/catalogcat.rb
ADDED
@@ -13,8 +13,7 @@ describe Bearcat::Client::CanvasFiles do
|
|
13
13
|
stub_request(:post, "https://upload-url.invalid/").
|
14
14
|
to_return(status: 302, headers: {'Location' => 'https://confirm-upload.invalid/confirm?param=true'})
|
15
15
|
|
16
|
-
stub_get(@client,
|
17
|
-
with(:query => {"param" => ["true"]}).to_return(json_response('canvas_files', 'upload_success.json'))
|
16
|
+
stub_get(@client, '/confirm').with(query: { param: true }).to_return(json_response('canvas_files', 'upload_success.json'))
|
18
17
|
|
19
18
|
response = @client.upload_file('my/upload/path', fixture('bearcat.jpg'))
|
20
19
|
expect(response['id']).to eq 123
|
@@ -12,7 +12,7 @@ describe Bearcat::Client::ContentMigrations do
|
|
12
12
|
stub_request(:post, "http://host/files_api").
|
13
13
|
to_return(status: 302, headers: {'Location' => 'https://confirm-upload.invalid/confirm?param=true'})
|
14
14
|
|
15
|
-
stub_get(@client, '/confirm').with(:
|
15
|
+
stub_get(@client, '/confirm').with(query: { param: true }).to_return(json_response('content_migration_files/upload_success.json'))
|
16
16
|
|
17
17
|
opts = {migration_type: 'canvas_cartridge_importer', pre_attachment: {name: 'cc.imscc', size: '2034'}}
|
18
18
|
response = @client.upload_content_package('my/upload/path', fixture('cc.imscc'), opts)
|
@@ -49,8 +49,7 @@ describe Bearcat::Client::Sections do
|
|
49
49
|
stub_request(:post, "http://host/files_api").
|
50
50
|
to_return(status: 302, headers: {'Location' => 'https://confirm-upload.invalid/confirm?param=true'})
|
51
51
|
|
52
|
-
stub_get(@client,
|
53
|
-
with(:query => {"param" => ["true"]}).to_return(json_response('content_migration_files', 'upload_success.json'))
|
52
|
+
stub_get(@client, '/confirm').with(query: { param: true }).to_return(json_response('content_migration_files', 'upload_success.json'))
|
54
53
|
|
55
54
|
opts = {'migration_type' => 'canvas_cartridge_importer', 'pre_attachment[name]' => 'cc.imscc', 'pre_attachment[size]' => '2034'}
|
56
55
|
response = @client.create_content_migration('659', fixture('cc.imscc'), opts)
|
@@ -66,8 +65,7 @@ describe Bearcat::Client::Sections do
|
|
66
65
|
stub_request(:post, "http://host/files_api").
|
67
66
|
to_return(status: 302, headers: {'Location' => 'https://confirm-upload.invalid/confirm?param=true'})
|
68
67
|
|
69
|
-
stub_get(@client,
|
70
|
-
with(:query => {"param" => ["true"]}).to_return(json_response('content_migration_files', 'upload_success.json'))
|
68
|
+
stub_get(@client, '/confirm').with(query: { param: true }).to_return(json_response('content_migration_files', 'upload_success.json'))
|
71
69
|
|
72
70
|
opts = {'migration_type' => 'canvas_cartridge_importer', 'pre_attachment[name]' => 'cc.imscc', 'pre_attachment[size]' => '2034'}
|
73
71
|
content_migration_response, file_upload_response = @client.create_content_migration_with_both_responses('659', fixture('cc.imscc'), opts)
|
File without changes
|
data/spec/bearcat/client_spec.rb
CHANGED
@@ -1,16 +1,13 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
describe Bearcat::Client do
|
4
|
-
|
5
4
|
it "sets the domain" do
|
6
5
|
client = Bearcat::Client.new(domain: "http://canvas.instructure.com")
|
7
6
|
client.config[:domain].should == "http://canvas.instructure.com"
|
8
|
-
|
9
7
|
end
|
10
8
|
|
11
9
|
it "sets the authtoken" do
|
12
10
|
client = Bearcat::Client.new(token: "test_token")
|
13
11
|
client.config[:token].should == "test_token"
|
14
12
|
end
|
15
|
-
|
16
|
-
end
|
13
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
require 'sidekiq'
|
4
|
+
|
5
|
+
describe Bearcat::RateLimiting do
|
6
|
+
let!(:subject) { Bearcat::RateLimiting::RedisLimiter.new }
|
7
|
+
let!(:client) { Bearcat::Client.new(rate_limiter: subject, token: SecureRandom.hex) }
|
8
|
+
let!(:token) { client.send(:rate_limit_key) }
|
9
|
+
|
10
|
+
def fillup(count, rate: 100)
|
11
|
+
count.times do
|
12
|
+
subject.apply(token) do
|
13
|
+
rate
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "fills the bucket with each request" do
|
19
|
+
allow(Time).to receive(:now).and_return(Time.at(5000))
|
20
|
+
expect(subject).to_not receive(:sleep)
|
21
|
+
fillup(5)
|
22
|
+
expect(subject.increment(token, 0, 0)).to eql({ count: 500.0, timestamp: 5000.0 })
|
23
|
+
end
|
24
|
+
|
25
|
+
it "drains the bucket over time" do
|
26
|
+
allow(Time).to receive(:now).and_return(Time.at(5000))
|
27
|
+
expect(subject).to_not receive(:sleep)
|
28
|
+
fillup(5)
|
29
|
+
|
30
|
+
allow(Time).to receive(:now).and_return(Time.at(5009))
|
31
|
+
expect(subject.increment(token, 0, 0)).to eql({ count: 410.0, timestamp: 5009.0 })
|
32
|
+
end
|
33
|
+
|
34
|
+
it "sleeps requests when full" do
|
35
|
+
allow(Time).to receive(:now).and_return(Time.at(5000))
|
36
|
+
expect(subject).to receive(:sleep).at_least(:once)
|
37
|
+
fillup(10)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "rate limits requests" do
|
41
|
+
allow(Time).to receive(:now).and_return(Time.at(5000))
|
42
|
+
allow(subject).to receive(:sleep)
|
43
|
+
fillup(10)
|
44
|
+
|
45
|
+
stub_request(:get, /.*/).to_return(body: "{}")
|
46
|
+
expect(subject).to receive(:sleep).once
|
47
|
+
client.course(1)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "retries after a Canvas 403 - Rate Limited" do
|
51
|
+
stub_request(:get, /.*/).to_return(status: 403, body: "(Rate Limit Exceeded)")
|
52
|
+
expect(subject).to receive(:apply).exactly(3).times.and_call_original
|
53
|
+
expect(subject).to receive(:sleep).twice
|
54
|
+
expect { client.course(1) }.to raise_error(Footrest::HttpError::Forbidden)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "pushes x-rate-limit-remaining to the bucket" do
|
58
|
+
stub_request(:get, /.*/).to_return(headers: { 'X-Rate-Limit-Remaining' => 120 }, body: "{}")
|
59
|
+
expect(subject).to receive(:checkin_known).with(token, 20)
|
60
|
+
client.course(1)
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
require 'bearcat/spec_helpers'
|
4
|
+
|
5
|
+
describe Bearcat::SpecHelpers do
|
6
|
+
include Bearcat::SpecHelpers
|
7
|
+
let!(:client) { Bearcat::Client.new(token: SecureRandom.hex) }
|
8
|
+
|
9
|
+
describe "#stub_bearcat" do
|
10
|
+
it "works as expected" do
|
11
|
+
stub_bearcat(:course).to_return(body: { id: 10 })
|
12
|
+
expect(client.course(1)[:id]).to eql 10
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/spec/helper.rb
CHANGED