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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/bearcat.gemspec +9 -3
  3. data/lib/bearcat/client/account_reports.rb +6 -14
  4. data/lib/bearcat/client/accounts.rb +20 -56
  5. data/lib/bearcat/client/analytics.rb +4 -7
  6. data/lib/bearcat/client/assignment_groups.rb +7 -19
  7. data/lib/bearcat/client/assignments.rb +15 -39
  8. data/lib/bearcat/client/blueprint_courses.rb +14 -20
  9. data/lib/bearcat/client/calendar_events.rb +9 -17
  10. data/lib/bearcat/client/canvas_files.rb +0 -2
  11. data/lib/bearcat/client/conferences.rb +3 -8
  12. data/lib/bearcat/client/conversations.rb +3 -8
  13. data/lib/bearcat/client/courses.rb +15 -45
  14. data/lib/bearcat/client/custom_gradebook_columns.rb +11 -25
  15. data/lib/bearcat/client/discussions.rb +11 -37
  16. data/lib/bearcat/client/enrollments.rb +9 -29
  17. data/lib/bearcat/client/external_tools.rb +9 -36
  18. data/lib/bearcat/client/file_helper.rb +2 -2
  19. data/lib/bearcat/client/files.rb +2 -4
  20. data/lib/bearcat/client/folders.rb +13 -59
  21. data/lib/bearcat/client/group_categories.rb +8 -17
  22. data/lib/bearcat/client/group_memberships.rb +6 -14
  23. data/lib/bearcat/client/groups.rb +9 -24
  24. data/lib/bearcat/client/module_items.rb +9 -17
  25. data/lib/bearcat/client/modules.rb +10 -20
  26. data/lib/bearcat/client/outcome_groups.rb +2 -4
  27. data/lib/bearcat/client/outcomes.rb +4 -7
  28. data/lib/bearcat/client/pages.rb +8 -23
  29. data/lib/bearcat/client/progresses.rb +2 -3
  30. data/lib/bearcat/client/quizzes.rb +15 -35
  31. data/lib/bearcat/client/reports.rb +8 -19
  32. data/lib/bearcat/client/roles.rb +1 -1
  33. data/lib/bearcat/client/rubric.rb +8 -20
  34. data/lib/bearcat/client/rubric_assessment.rb +5 -11
  35. data/lib/bearcat/client/rubric_association.rb +6 -12
  36. data/lib/bearcat/client/search.rb +2 -4
  37. data/lib/bearcat/client/sections.rb +11 -26
  38. data/lib/bearcat/client/submissions.rb +13 -36
  39. data/lib/bearcat/client/tabs.rb +4 -7
  40. data/lib/bearcat/client/users.rb +17 -44
  41. data/lib/bearcat/client.rb +72 -44
  42. data/lib/bearcat/client_module.rb +101 -0
  43. data/lib/bearcat/rate_limiting/redis_script.rb +164 -0
  44. data/lib/bearcat/rate_limiting.rb +70 -0
  45. data/lib/bearcat/spec_helpers.rb +62 -31
  46. data/lib/bearcat/version.rb +1 -1
  47. data/lib/bearcat.rb +8 -21
  48. data/lib/catalogcat/api_array.rb +18 -0
  49. data/lib/catalogcat/client/catalogs.rb +21 -0
  50. data/lib/catalogcat/client/certificates.rb +9 -0
  51. data/lib/catalogcat/client/courses.rb +25 -0
  52. data/lib/catalogcat/client/orders.rb +9 -0
  53. data/lib/catalogcat/client.rb +39 -0
  54. data/lib/catalogcat/version.rb +3 -0
  55. data/lib/catalogcat.rb +14 -0
  56. data/spec/bearcat/client/canvas_files_spec.rb +1 -2
  57. data/spec/bearcat/client/content_migrations_spec.rb +1 -1
  58. data/spec/bearcat/client/courses_spec.rb +2 -4
  59. data/spec/bearcat/{group_memberships_spec.rb → client/group_memberships_spec.rb} +0 -0
  60. data/spec/bearcat/client_spec.rb +1 -4
  61. data/spec/bearcat/rate_limiting_spec.rb +62 -0
  62. data/spec/bearcat/stub_bearcat_spec.rb +15 -0
  63. data/spec/helper.rb +1 -0
  64. 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
@@ -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
- url = case endpoint
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(resolve_prefix(prefix)) + url
48
- stub = stub_request(method == :auto ? (match ? match[:method].to_sym : :get) : method, Regexp.new(url))
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 resolve_prefix(prefix)
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
@@ -1,3 +1,3 @@
1
1
  module Bearcat
2
- VERSION = '1.4.13' unless defined?(Bearcat::VERSION)
2
+ VERSION = '1.5.0.beta1' unless defined?(Bearcat::VERSION)
3
3
  end
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 :enforce_rate_limits, :rate_limit_min, :rate_limits, :max_sleep_seconds,
8
- :min_sleep_seconds, :logger, :master_rate_limit, :master_mutex,
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 rate_limit_min
16
- @rate_limit_min ||= 175
14
+ def rate_limiter
15
+ @rate_limiter
17
16
  end
18
17
 
19
- def enforce_rate_limits
20
- @enforce_rate_limits ||= false
18
+ def rate_limit_min
19
+ @rate_limit_min ||= 175
21
20
  end
22
21
 
23
- def rate_limits
24
- @rate_limits ||= {}
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,9 @@
1
+ module Catalogcat
2
+ class Client < Footrest::Client
3
+ module Courses
4
+ def create_certificate(params = {})
5
+ post('/api/v1/certificates', params)
6
+ end
7
+ end
8
+ end
9
+ 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,9 @@
1
+ module Catalogcat
2
+ class Client < Footrest::Client
3
+ module Orders
4
+ def get_order(id, params = {})
5
+ get("api/v1/orders/#{id}", params)
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,3 @@
1
+ module Catalogcat
2
+ VERSION = 'bearcat' unless defined?(Catalogcat::VERSION)
3
+ end
data/lib/catalogcat.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'catalogcat/client'
2
+
3
+ module Catalogcat
4
+ class << self
5
+ require 'logger'
6
+
7
+ def logger
8
+ return @logger if defined? @logger
9
+ @logger = Logger.new(STDOUT)
10
+ @logger.level = Logger::DEBUG
11
+ @logger
12
+ end
13
+ end
14
+ end
@@ -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, "/confirm").
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(:query => {"param" => ["true"]}).to_return(json_response('content_migration_files/upload_success.json'))
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, "/confirm").
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, "/confirm").
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)
@@ -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
@@ -1,3 +1,4 @@
1
+ require 'active_support/core_ext/module'
1
2
  require 'bearcat'
2
3
  require 'rspec'
3
4
  require 'webmock/rspec'