bearcat 1.4.13 → 1.5.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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'