bearcat 1.4.12 → 1.5.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/bearcat.gemspec +9 -3
  3. data/lib/bearcat/api_array.rb +2 -0
  4. data/lib/bearcat/client/account_reports.rb +6 -14
  5. data/lib/bearcat/client/accounts.rb +20 -56
  6. data/lib/bearcat/client/analytics.rb +4 -7
  7. data/lib/bearcat/client/assignment_groups.rb +7 -19
  8. data/lib/bearcat/client/assignments.rb +15 -39
  9. data/lib/bearcat/client/blueprint_courses.rb +14 -20
  10. data/lib/bearcat/client/calendar_events.rb +9 -17
  11. data/lib/bearcat/client/canvas_files.rb +0 -2
  12. data/lib/bearcat/client/conferences.rb +3 -8
  13. data/lib/bearcat/client/conversations.rb +3 -8
  14. data/lib/bearcat/client/courses.rb +15 -45
  15. data/lib/bearcat/client/custom_gradebook_columns.rb +11 -25
  16. data/lib/bearcat/client/discussions.rb +11 -37
  17. data/lib/bearcat/client/enrollments.rb +9 -29
  18. data/lib/bearcat/client/external_tools.rb +9 -36
  19. data/lib/bearcat/client/file_helper.rb +3 -3
  20. data/lib/bearcat/client/files.rb +2 -4
  21. data/lib/bearcat/client/folders.rb +13 -59
  22. data/lib/bearcat/client/group_categories.rb +8 -17
  23. data/lib/bearcat/client/group_memberships.rb +6 -14
  24. data/lib/bearcat/client/groups.rb +9 -24
  25. data/lib/bearcat/client/module_items.rb +9 -17
  26. data/lib/bearcat/client/modules.rb +10 -20
  27. data/lib/bearcat/client/outcome_groups.rb +2 -4
  28. data/lib/bearcat/client/outcomes.rb +4 -7
  29. data/lib/bearcat/client/pages.rb +8 -23
  30. data/lib/bearcat/client/progresses.rb +2 -3
  31. data/lib/bearcat/client/quizzes.rb +15 -35
  32. data/lib/bearcat/client/reports.rb +8 -19
  33. data/lib/bearcat/client/roles.rb +1 -1
  34. data/lib/bearcat/client/rubric.rb +8 -20
  35. data/lib/bearcat/client/rubric_assessment.rb +5 -11
  36. data/lib/bearcat/client/rubric_association.rb +6 -12
  37. data/lib/bearcat/client/search.rb +2 -4
  38. data/lib/bearcat/client/sections.rb +11 -26
  39. data/lib/bearcat/client/submissions.rb +48 -44
  40. data/lib/bearcat/client/tabs.rb +4 -7
  41. data/lib/bearcat/client/users.rb +17 -44
  42. data/lib/bearcat/client.rb +72 -44
  43. data/lib/bearcat/client_module.rb +101 -0
  44. data/lib/bearcat/rate_limiting/redis_script.rb +164 -0
  45. data/lib/bearcat/rate_limiting.rb +70 -0
  46. data/lib/bearcat/spec_helpers.rb +62 -31
  47. data/lib/bearcat/version.rb +1 -1
  48. data/lib/bearcat.rb +8 -21
  49. data/lib/catalogcat/api_array.rb +18 -0
  50. data/lib/catalogcat/client/catalogs.rb +21 -0
  51. data/lib/catalogcat/client/certificates.rb +9 -0
  52. data/lib/catalogcat/client/courses.rb +25 -0
  53. data/lib/catalogcat/client/orders.rb +9 -0
  54. data/lib/catalogcat/client.rb +39 -0
  55. data/lib/catalogcat/version.rb +3 -0
  56. data/lib/catalogcat.rb +14 -0
  57. data/spec/bearcat/client/canvas_files_spec.rb +1 -2
  58. data/spec/bearcat/client/content_migrations_spec.rb +2 -2
  59. data/spec/bearcat/client/courses_spec.rb +2 -4
  60. data/spec/bearcat/{group_memberships_spec.rb → client/group_memberships_spec.rb} +0 -0
  61. data/spec/bearcat/client/learning_outcomes_spec.rb +2 -2
  62. data/spec/bearcat/client/submissions_spec.rb +14 -0
  63. data/spec/bearcat/client_spec.rb +1 -4
  64. data/spec/bearcat/rate_limiting_spec.rb +62 -0
  65. data/spec/bearcat/stub_bearcat_spec.rb +15 -0
  66. data/spec/helper.rb +1 -0
  67. metadata +196 -181
@@ -0,0 +1,164 @@
1
+ require 'pathname'
2
+ require 'digest/sha1'
3
+ require 'erb'
4
+
5
+ # Modified from https://github.com/Shopify/wolverine/blob/master/lib/wolverine/script.rb
6
+
7
+ module Bearcat
8
+ module RateLimiting
9
+ # {RedisScript} represents a lua script in the filesystem. It loads the script
10
+ # from disk and handles talking to redis to execute it. Error handling
11
+ # is handled by {LuaError}.
12
+ class RedisScript
13
+
14
+ # Loads the script file from disk and calculates its +SHA1+ sum.
15
+ #
16
+ # @param file [Pathname] the full path to the indicated file
17
+ def initialize(file)
18
+ @file = Pathname.new(file)
19
+ end
20
+
21
+ # Passes the script and supplied arguments to redis for evaulation.
22
+ # It first attempts to use a script redis has already cached by using
23
+ # the +EVALSHA+ command, but falls back to providing the full script
24
+ # text via +EVAL+ if redis has not seen this script before. Future
25
+ # invocations will then use +EVALSHA+ without erroring.
26
+ #
27
+ # @param redis [Redis] the redis connection to run against
28
+ # @param args [*Objects] the arguments to the script
29
+ # @return [Object] the value passed back by redis after script execution
30
+ # @raise [LuaError] if the script failed to compile of encountered a
31
+ # runtime error
32
+ def call(redis, *args)
33
+ t = Time.now
34
+ begin
35
+ redis.evalsha(digest, *args)
36
+ rescue => e
37
+ e.message =~ /NOSCRIPT/ ? redis.eval(content, *args) : raise
38
+ end
39
+ rescue => e
40
+ if LuaError.intercepts?(e)
41
+ raise LuaError.new(e, @file, content)
42
+ else
43
+ raise
44
+ end
45
+ end
46
+
47
+ def content
48
+ @content ||= load_lua(@file)
49
+ end
50
+
51
+ def digest
52
+ @digest ||= Digest::SHA1.hexdigest content
53
+ end
54
+
55
+ private
56
+
57
+ def script_path
58
+ @file.basename
59
+ # Rails.root + 'app/redis_lua'
60
+ end
61
+
62
+ def relative_path
63
+ @path ||= @file.relative_path_from(script_path)
64
+ end
65
+
66
+ def load_lua(file)
67
+ TemplateContext.new(script_path).template(script_path + file)
68
+ end
69
+
70
+ class TemplateContext
71
+ def initialize(script_path)
72
+ @script_path = script_path
73
+ end
74
+
75
+ def template(pathname)
76
+ @partial_templates ||= {}
77
+ ERB.new(File.read(pathname)).result binding
78
+ end
79
+
80
+ # helper method to include a lua partial within another lua script
81
+ #
82
+ # @param relative_path [String] the relative path to the script from
83
+ # `script_path`
84
+ def include_partial(relative_path)
85
+ unless @partial_templates.has_key? relative_path
86
+ @partial_templates[relative_path] = nil
87
+ template( Pathname.new("#{@script_path}/#{relative_path}") )
88
+ end
89
+ end
90
+ end
91
+
92
+ # Reformats errors raised by redis representing failures while executing
93
+ # a lua script. The default errors have confusing messages and backtraces,
94
+ # and a type of +RuntimeError+. This class improves the message and
95
+ # modifies the backtrace to include the lua script itself in a reasonable
96
+ # way.
97
+ class LuaError < StandardError
98
+ PATTERN = /ERR Error (compiling|running) script \(.*?\): .*?:(\d+): (.*)/
99
+ WOLVERINE_LIB_PATH = File.expand_path('../../', __FILE__)
100
+ CONTEXT_LINE_NUMBER = 2
101
+
102
+ attr_reader :error, :file, :content
103
+
104
+ # Is this error one that should be reformatted?
105
+ #
106
+ # @param error [StandardError] the original error raised by redis
107
+ # @return [Boolean] is this an error that should be reformatted?
108
+ def self.intercepts? error
109
+ error.message =~ PATTERN
110
+ end
111
+
112
+ # Initialize a new {LuaError} from an existing redis error, adjusting
113
+ # the message and backtrace in the process.
114
+ #
115
+ # @param error [StandardError] the original error raised by redis
116
+ # @param file [Pathname] full path to the lua file the error ocurred in
117
+ # @param content [String] lua file content the error ocurred in
118
+ def initialize error, file, content
119
+ @error = error
120
+ @file = file
121
+ @content = content
122
+
123
+ @error.message =~ PATTERN
124
+ _stage, line_number, message = $1, $2, $3
125
+ error_context = generate_error_context(content, line_number.to_i)
126
+
127
+ super "#{message}\n\n#{error_context}\n\n"
128
+ set_backtrace generate_backtrace file, line_number
129
+ end
130
+
131
+ private
132
+
133
+ def generate_error_context(content, line_number)
134
+ lines = content.lines.to_a
135
+ beginning_line_number = [1, line_number - CONTEXT_LINE_NUMBER].max
136
+ ending_line_number = [lines.count, line_number + CONTEXT_LINE_NUMBER].min
137
+ line_number_width = ending_line_number.to_s.length
138
+
139
+ (beginning_line_number..ending_line_number).map do |number|
140
+ indicator = number == line_number ? '=>' : ' '
141
+ formatted_number = "%#{line_number_width}d" % number
142
+ " #{indicator} #{formatted_number}: #{lines[number - 1]}"
143
+ end.join.chomp
144
+ end
145
+
146
+ def generate_backtrace(file, line_number)
147
+ pre_wolverine = backtrace_before_entering_wolverine(@error.backtrace)
148
+ index_of_first_wolverine_line = (@error.backtrace.size - pre_wolverine.size - 1)
149
+ pre_wolverine.unshift(@error.backtrace[index_of_first_wolverine_line])
150
+ pre_wolverine.unshift("#{file}:#{line_number}")
151
+ pre_wolverine
152
+ end
153
+
154
+ def backtrace_before_entering_wolverine(backtrace)
155
+ backtrace.reverse.take_while { |line| ! line_from_wolverine(line) }.reverse
156
+ end
157
+
158
+ def line_from_wolverine(line)
159
+ line.split(':').first.include?(WOLVERINE_LIB_PATH)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -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.12' 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_post(@client, "/confirm").
17
- with(:body => {"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
@@ -7,12 +7,12 @@ describe Bearcat::Client::ContentMigrations do
7
7
 
8
8
  it 'uploads a file' do
9
9
  stub_post(@client, "/my/upload/path").
10
- to_return(json_response('content_migration_files/response.json'))
10
+ to_return(json_response('content_migration_files/response.json'))
11
11
 
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_post(@client, '/confirm').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_post(@client, "/confirm").
53
- with(:body => {"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_post(@client, "/confirm").
70
- with(:body => {"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)
@@ -8,7 +8,7 @@ describe Bearcat::Client::LearningOutcomes do
8
8
  end
9
9
 
10
10
  it "returns an individual learning outcome" do
11
- stub_get(@client, "/api/v1/outcomes/1").to_return(json_response("learning_outcomes.json"))
11
+ stub_get(@client, "/api/v1/outcomes/1").to_return(json_response("learning_outcome.json"))
12
12
  outcome = @client.learning_outcome(1)
13
13
  outcome["id"].should == 1
14
14
  outcome["context_type"].should == "Course"
@@ -16,7 +16,7 @@ describe Bearcat::Client::LearningOutcomes do
16
16
  end
17
17
 
18
18
  it "updates an individual learning outcome" do
19
- stub_put(@client, "/api/v1/outcomes/1").to_return(json_response("learning_outcomes.json"))
19
+ stub_put(@client, "/api/v1/outcomes/1").to_return(json_response("learning_outcome.json"))
20
20
  outcome = @client.update_learning_outcome(1)
21
21
  outcome["id"].should == 1
22
22
  outcome["context_type"].should == "Course"
@@ -45,6 +45,13 @@ describe Bearcat::Client::Submissions do
45
45
  response['id'].should == 8444510
46
46
  end
47
47
 
48
+ it "submits multiple files" do
49
+ @client.stub(:upload_file).and_return({})
50
+ stub_post(@client, '/api/v1/courses/1/assignments/2/submissions').to_return(json_response('submissions', 'submission.json'))
51
+ response = @client.course_file_upload_submission(1, 2, 3, [fixture('bearcat.jpg'), fixture('file.csv')])
52
+ response['id'].should == 8444510
53
+ end
54
+
48
55
  it "updates grades" do
49
56
  params = {"grade_data[123]" => "19"}
50
57
  stub_post(@client, "/api/v1/courses/1/assignments/1/submissions/update_grades").with(body: {"grade_data"=>["19"]}).to_return(json_response("progress.json"))
@@ -56,6 +63,13 @@ describe Bearcat::Client::Submissions do
56
63
 
57
64
  context 'section' do
58
65
  it "submits a file" do
66
+ @client.stub(:upload_file).and_return({})
67
+ stub_post(@client, '/api/v1/sections/1/assignments/2/submissions').to_return(json_response('submissions', 'submission.json'))
68
+ response = @client.section_file_upload_submission(1, 2, 3, [fixture('bearcat.jpg'), fixture('file.csv')])
69
+ response['id'].should == 8444510
70
+ end
71
+
72
+ it "submits multiple files" do
59
73
  @client.stub(:upload_file).and_return({})
60
74
  stub_post(@client, '/api/v1/sections/1/assignments/2/submissions').to_return(json_response('submissions', 'submission.json'))
61
75
  response = @client.section_file_upload_submission(1, 2, 3, fixture('bearcat.jpg'))
@@ -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