├── .gitignore
├── Changelog
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── canvas-api.gemspec
├── lib
└── canvas-api.rb
└── spec
├── custom_ids_spec.rb
├── file_uploads_spec.rb
├── get_requests_spec.rb
├── init_spec.rb
├── masquerading_spec.rb
├── non_get_requests_spec.rb
├── oauth_spec.rb
├── pagination_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
--------------------------------------------------------------------------------
/Changelog:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whitmer/canvas-api/94c8db69ad8fbeda8bafdee78501a9085e81c3e0/Changelog
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gem 'json'
4 | gem 'typhoeus'
5 |
6 | group :test do
7 | gem 'rspec', :require => "spec"
8 | end
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: http://rubygems.org/
3 | specs:
4 | diff-lcs (1.2.4)
5 | ethon (0.7.0)
6 | ffi (>= 1.3.0)
7 | ffi (1.9.3)
8 | json (1.7.7)
9 | rspec (2.13.0)
10 | rspec-core (~> 2.13.0)
11 | rspec-expectations (~> 2.13.0)
12 | rspec-mocks (~> 2.13.0)
13 | rspec-core (2.13.1)
14 | rspec-expectations (2.13.0)
15 | diff-lcs (>= 1.1.3, < 2.0)
16 | rspec-mocks (2.13.1)
17 | typhoeus (0.6.8)
18 | ethon (>= 0.7.0)
19 |
20 | PLATFORMS
21 | ruby
22 |
23 | DEPENDENCIES
24 | json
25 | rspec
26 | typhoeus
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Instructure
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to use,
6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
7 | Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Canvas API
2 |
3 | This ruby library is to make it easier to use the
4 | [Canvas API](http://api.instructure.com).
5 |
6 | ## Installation
7 | This is packaged as the `canvas-api` rubygem, so you can just add the dependency to
8 | your Gemfile or install the gem on your system:
9 |
10 | gem install canvas-api
11 |
12 | To require the library in your project:
13 |
14 | require 'canvas-api'
15 |
16 | ## Usage
17 |
18 | ### OAuth Dance
19 |
20 | Before you can make API calls you need an access token on behalf of the current user.
21 | In order to get an access token you'll need to do the OAuth dance (and for that you'll
22 | need a client_id and secret. Talk to the Canvas admin about getting these values):
23 |
24 | ```ruby
25 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :client_id => 123, :secret => "abcdef")
26 | url = canvas.oauth_url("https://my.site/oauth_success")
27 | # => "https://canvas.example.com/login/oauth2/auth?client_id=123&response_type=code&redirect_uri=http%3A%2F%2Fmy.site%2Foauth_success
28 | redirect to(url)
29 | ```
30 |
31 | And then when the browser redirects to oauth_success:
32 |
33 | ```ruby
34 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :client_id => 123, :secret => "abcdef")
35 | code = params['code']
36 | canvas.retrieve_access_token(code, 'https://my.site/oauth_success') # this callback_url must match the one provided in the first step
37 | # => {access_token: "qwert"}
38 | ```
39 | ### General API Calls
40 |
41 | Once you've got an access token for a user you should save it (securely!) for future use. To use the API call:
42 |
43 | ```ruby
44 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
45 | canvas.get("/api/v1/users/self/profile")
46 | # => {id: 90210, name: "Annie Wilson", ... }
47 | ```
48 |
49 | For POST and PUT requests the second parameter is the form parameters to append, either as a hash or
50 | an array of arrays:
51 |
52 | ```ruby
53 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
54 | canvas.put("/api/v1/users/self", {'user[name]' => 'Dixon Wilson', 'user[short_name]' => 'Dixon'})
55 | # => {id: 90210, name: "Dixon Wilson", ... }
56 | canvas.put("/api/v1/users/self", {'user' => {'name' => 'Dixon Wilson', 'short_name' => 'Dixon'}}) # this is synonymous with the previous call
57 | # => {id: 90210, name: "Dixon Wilson", ... }
58 | canvas.put("/api/v1/users/self", [['user[name]', 'Dixon Wilson'],['user[short_name]', 'Dixon']]) # this is synonymous with the previous call
59 | # => {id: 90210, name: "Dixon Wilson", ... }
60 | ```
61 |
62 | On GET requests you can either append query parameters to the actual path or as a hashed second argument:
63 |
64 | ```ruby
65 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
66 | canvas.get("/api/v1/users/self/enrollments?type[]=TeacherEnrollment&type[]=TaEnrollment")
67 | # => [{id: 1234, course_id: 5678, ... }, {id: 2345, course_id: 6789, ...}]
68 | canvas.get("/api/v1/users/self/enrollments", {'type' => ['TeacherEnrollment', 'TaEnrollment']}) # this is synonymous with the previous call
69 | # => [{id: 1234, course_id: 5678, ... }, {id: 2345, course_id: 6789, ...}]
70 | ```
71 |
72 | ### Pagination
73 |
74 | API endpoints that return lists are often paginated, meaning they will only return the first X results
75 | (where X depends on the endpoint and, possibly, the per_page parameter you optionally set). To get more
76 | results you'll need to make additional API calls:
77 |
78 | ```ruby
79 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
80 | list = canvas.get("/api/v1/calendar_events?all_events=true")
81 | list.length
82 | # => 50
83 | list.more?
84 | # => true (if there's another page of results)
85 | list.next_page!
86 | # => [...] (returns the next page of results)
87 | list.length
88 | # => 100 (also concatenates the results on to the previous list, if that's more convenient)
89 | list.next_page!
90 | # => [...]
91 | list.length
92 | # => 150
93 | ```
94 |
95 | ### Additional Utilities
96 |
97 | There are also some helper methods that can make some of the other tricky parts of the Canvas API a little more approachable.
98 |
99 | #### File Uploads
100 |
101 | Uploading files ia typically a multi-step process. There are three different ways to upload
102 | files.
103 |
104 | Upload a file from the local file system:
105 |
106 |
107 | ```ruby
108 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
109 | canvas.upload_file_from_local("/api/v1/users/self/files", File.open("/path/to/file.jpg"), :content_type => "image/jpeg")
110 | # => {id: 1, display_name: "file.jpg", ... }
111 | ```
112 |
113 | Upload a file synchronously from a remote URL:
114 |
115 | ```ruby
116 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
117 | canvas.upload_file_from_url("/api/v1/users/self/files", :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
118 | # => {id: 1, display_name: "image.jpg", ... }
119 | ```
120 |
121 | Upload a file asynchronouysly from a remote URL:
122 |
123 | ```ruby
124 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
125 | status_url = canvas.upload_file_from_url("/api/v1/users/self/files", :asynch => true, :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
126 | # => "/api/v1/file_status/url"
127 | canvas.get(status_url)
128 | # => {upload_status: "pending"}
129 | canvas.get(status_url)
130 | # => {upload_status: "ready", attachment: {id: 1, display_name: "image.jpg", ... } }
131 | ```
132 |
133 | ```ruby
134 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
135 | status_url = canvas.upload_file_from_url("/api/v1/users/self/files", :asynch => true, :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
136 | # => "/api/v1/file_status/url"
137 | canvas.get(status_url)
138 | # => {upload_status: "errored", message: "Invalid response code, expected 200 got 404"}
139 | ```
140 |
141 | For any of these upload types you can optionally provide additional configuration parameters if
142 | the upload endpoint is to an area of Canvas that supports folders (user files, course files, etc.)
143 |
144 | ```ruby
145 | canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
146 | #
147 | # upload the file to a known folder with id 1234
148 | canvas.upload_file_from_url("/api/v1/users/self/files", :parent_folder_id => 1234, :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
149 | # => {id: 1, display_name: "image.jpg", ... }
150 | #
151 | # upload the file to a folder with the path "/friends"
152 | canvas.upload_file_from_url("/api/v1/users/self/files", :parent_folder_path => "/friends", :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
153 | # => {id: 1, display_name: "image.jpg", ... }
154 | #
155 | # rename this file instead of overwriting a file with the same name (overwrite is the default)
156 | canvas.upload_file_from_url("/api/v1/users/self/files", :on_duplicate => "rename", :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
157 | # => {id: 1, display_name: "image.jpg", ... }
158 | ```
159 |
160 |
161 |
162 | #### SIS ID Encoding
163 |
164 | In addition to regular IDs, Canvas supports [SIS IDs](https://canvas.instructure.com/doc/api/file.object_ids.html) defined
165 | by other systems. Sometimes these IDs contain non-standard characters, which can cause problems when
166 | trying to use them via the API. In those cases you can do the following:
167 |
168 | ```ruby
169 | sis_course_id = canvas.encode_id("sis_course_id", "r#-789")
170 | # => "hex:sis_course_id:72232d373839"
171 | canvas.get("/api/v1/courses/#{sis_course_id}/enrollments")
172 | # => [...]
173 | ```
174 |
175 |
--------------------------------------------------------------------------------
/canvas-api.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = %q{canvas-api}
3 | s.version = "1.1.1"
4 |
5 | s.add_dependency 'json'
6 | s.add_dependency 'typhoeus'
7 |
8 | s.add_development_dependency 'rspec'
9 | s.add_development_dependency 'ruby-debug'
10 |
11 | s.authors = ["Instructure"]
12 | s.date = %q{2014-05-15}
13 | s.extra_rdoc_files = %W(LICENSE)
14 | s.files = Dir["{lib}/**/*"] + ["LICENSE", "README.md", "Changelog"]
15 | s.homepage = %q{http://github.com/whitmer/canvas-api}
16 | s.require_paths = %W(lib)
17 | s.summary = %q{Ruby library for accessing the Canvas API}
18 | end
--------------------------------------------------------------------------------
/lib/canvas-api.rb:
--------------------------------------------------------------------------------
1 | require 'uri'
2 | require 'cgi'
3 | require 'net/http'
4 | require 'json'
5 | require 'typhoeus'
6 |
7 | module Canvas
8 | class API
9 | def initialize(args={})
10 | @host = args[:host] && args[:host].to_s
11 | @token = args[:token] && args[:token].to_s
12 | @client_id = args[:client_id] && args[:client_id].to_s
13 | @secret = args[:secret] && args[:secret].to_s
14 | @insecure = !!args[:insecure]
15 | raise "host required" unless @host
16 | raise "invalid host, protocol required" unless @host.match(/^http/)
17 | raise "invalid host" unless @host.match(/^https?:\/\/[^\/]+$/)
18 | raise "token or client_id required" if !@token && !@client_id
19 | raise "secret required for client_id configuration" if @client_id && !@secret
20 | end
21 |
22 | attr_accessor :host
23 | attr_accessor :token
24 | attr_accessor :client_id
25 |
26 | def masquerade_as(user_id)
27 | @as_user_id = user_id && user_id.to_s
28 | end
29 |
30 | def stop_masquerading
31 | @as_user_id = nil
32 | end
33 |
34 | def self.encode_id(prefix, id)
35 | return nil unless prefix && id
36 | "hex:#{prefix}:" + id.to_s.unpack("H*")[0]
37 | end
38 |
39 | def encode_id(prefix, id)
40 | Canvas::API.encode_id(prefix, id)
41 | end
42 |
43 | def oauth_url(callback_url, scopes="")
44 | raise "client_id required for oauth flow" unless @client_id
45 | raise "secret required for oauth flow" unless @secret
46 | raise "callback_url required" unless callback_url
47 | raise "invalid callback_url" unless (URI.parse(callback_url) rescue nil)
48 | scopes ||= ""
49 | scopes = scopes.length > 0 ? "&scopes=#{CGI.escape(scopes)}" : ""
50 | "#{@host}/login/oauth2/auth?client_id=#{@client_id}&response_type=code&redirect_uri=#{CGI.escape(callback_url)}#{scopes}"
51 | end
52 |
53 | def login_url(callback_url)
54 | oauth_url(callback_url, "/auth/userinfo")
55 | end
56 |
57 | def retrieve_access_token(code, callback_url)
58 | raise "client_id required for oauth flow" unless @client_id
59 | raise "secret required for oauth flow" unless @secret
60 | raise "code required" unless code
61 | raise "callback_url required" unless callback_url
62 | raise "invalid callback_url" unless (URI.parse(callback_url) rescue nil)
63 | @token = "ignore"
64 | res = post("/login/oauth2/token", :client_id => @client_id, :redirect_uri => callback_url, :client_secret => @secret, :code => code)
65 | if res['access_token']
66 | @token = res['access_token']
67 | end
68 | res
69 | end
70 |
71 | def logout
72 | !!delete("/login/oauth2/token")['logged_out']
73 | end
74 |
75 | def validate_call(endpoint)
76 | raise "token required for api calls" unless @token
77 | raise "missing host" unless @host
78 | raise "missing endpoint" unless endpoint
79 | raise "missing leading slash on endpoint" unless endpoint.match(/^\//)
80 | raise "invalid endpoint" unless endpoint.match(/^\/api\/v\d+\//) unless @token == 'ignore'
81 | raise "invalid endpoint" unless (URI.parse(endpoint) rescue nil)
82 | end
83 |
84 | def generate_uri(endpoint, params=nil)
85 | validate_call(endpoint)
86 | unless @token == "ignore"
87 | endpoint += (endpoint.match(/\?/) ? "&" : "?") + "access_token=" + @token
88 | endpoint += "&as_user_id=" + @as_user_id.to_s if @as_user_id
89 | end
90 | (params || {}).each do |key, value|
91 | if value.is_a?(Array)
92 | key = key + "[]" unless key.match(/\[\]/)
93 | value.each do |val|
94 | endpoint += (endpoint.match(/\?/) ? "&" : "?") + "#{CGI.escape(key.to_s)}=#{CGI.escape(val.to_s)}"
95 | end
96 | else
97 | endpoint += (endpoint.match(/\?/) ? "&" : "?") + "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
98 | end
99 | end
100 | @uri = URI.parse(@host + endpoint)
101 | @http = Net::HTTP.new(@uri.host, @uri.port)
102 | @http.use_ssl = @uri.scheme == 'https'
103 | @uri
104 | end
105 |
106 | def retrieve_response(request)
107 | request.options[:headers]['User-Agent'] = "CanvasAPI Ruby"
108 | if @insecure
109 | request.options[:ssl_verifypeer] = false
110 | end
111 | begin
112 | response = request.run
113 | raise ApiError.new("request timed out") if response.timed_out?
114 | rescue Timeout::Error => e
115 | raise ApiError.new("request timed out")
116 | end
117 | raise ApiError.new("unexpected redirect to #{response.headers['Location']}") if response.code.to_s.match(/3\d\d/)
118 | json = JSON.parse(response.body) rescue {'error' => 'invalid JSON'}
119 | if !json.is_a?(Array)
120 | raise ApiError.new(json['error']) if json['error']
121 | raise ApiError.new(json['errors']) if json['errors']
122 | if !response.code.to_s.match(/2\d\d/)
123 | json['message'] ||= "unexpected error"
124 | json['status'] ||= response.code.to_s
125 | raise ApiError.new("#{json['status']} #{json['message']}")
126 | end
127 | else
128 | json = ResultSet.new(self, json)
129 | if response.headers['Link']
130 | json.link = response.headers['Link']
131 | json.next_endpoint = response.headers['Link'].split(/,/).detect{|rel| rel.match(/rel="next"/) }.split(/;/).first.strip[1..-2].sub(/https?:\/\/[^\/]+/, '') rescue nil
132 | end
133 | end
134 | json
135 | end
136 |
137 | # Semi-hack so I can write better specs
138 | def get_request(endpoint)
139 | Typhoeus::Request.new(@uri.to_s, method: :get)
140 | end
141 |
142 | def get(endpoint, params=nil)
143 | generate_uri(endpoint, params)
144 | request = get_request(endpoint)
145 | retrieve_response(request)
146 | end
147 |
148 | def delete(endpoint, params={})
149 | query_parameters = params.is_a?(Hash) ? params['query_parameters'] || params[:query_parameters] : {}
150 | generate_uri(endpoint, query_parameters)
151 | request = Typhoeus::Request.new(@uri.to_s, method: :delete)
152 | request.options[:body] = clean_params(params)
153 | retrieve_response(request)
154 | end
155 |
156 | def put(endpoint, params={})
157 | query_parameters = params.is_a?(Hash) ? params['query_parameters'] || params[:query_parameters] : {}
158 | generate_uri(endpoint, query_parameters)
159 | request = Typhoeus::Request.new(@uri.to_s, method: :put)
160 | request.options[:body] = clean_params(params)
161 | retrieve_response(request)
162 | end
163 |
164 | def post(endpoint, params={})
165 | query_parameters = params.is_a?(Hash) ? params['query_parameters'] || params[:query_parameters] : {}
166 | generate_uri(endpoint, query_parameters)
167 | request = Typhoeus::Request.new(@uri.to_s, method: :post)
168 | request.options[:body] = params #clean_params(params)
169 | retrieve_response(request)
170 | end
171 |
172 | def post_multi(endpoint, params={})
173 | query_parameters = params.is_a?(Hash) ? params['query_parameters'] || params[:query_parameters] : {}
174 | generate_uri(endpoint, query_parameters)
175 | request = Typhoeus::Request.new(@uri.to_s, method: :post)
176 | request.options[:body] = clean_params(params)
177 | retrieve_response(request)
178 | end
179 |
180 | def clean_params(params, prefix=nil)
181 | params ||= {}
182 | return params if params.is_a?(Array)
183 | return nil unless params.is_a?(Hash)
184 | params.delete(:query_parameters)
185 | res = PairArray.new
186 | params.each do |key, val|
187 | if val.is_a?(Array)
188 | raise "No support for nested array parameters currently"
189 | elsif val.is_a?(Hash)
190 | res.concat clean_params(val, prefix ? (prefix + "[" + key.to_s + "]") : key.to_s)
191 | else
192 | if prefix
193 | res << [prefix + "[" + key.to_s + "]", val.to_s]
194 | else
195 | res << [key.to_s, val.to_s]
196 | end
197 | end
198 | end
199 | res
200 | end
201 |
202 | def upload_file_from_local(endpoint, file, opts={})
203 | raise "Missing File object" unless file.is_a?(File)
204 | params = {
205 | :size => file.size,
206 | :name => opts[:name] || opts['name'] || File.basename(file.path),
207 | :content_type => opts[:content_type] || opts['content_type'] || "application/octet-stream",
208 | :on_duplicate => opts[:on_duplicate] || opts['on_duplicate']
209 | }
210 | if opts[:parent_folder_id] || opts['parent_folder_id']
211 | params[:parent_folder_id] = opts[:parent_folder_id] || opts['parent_folder_id']
212 | elsif opts[:parent_folder_path] || opts['parent_folder_path']
213 | params[:parent_folder_path] = opts[:parent_folder_path] || opts['parent_folder_path']
214 | end
215 |
216 | res = post(endpoint, params)
217 | if !res['upload_url']
218 | raise ApiError.new("Unexpected error: #{res['message'] || 'no upload URL returned'}")
219 | end
220 | status_url = multipart_upload(res['upload_url'], res['upload_params'], params, file)
221 | status_path = "/" + status_url.split(/\//, 4)[-1]
222 | res = get(status_path)
223 | res
224 | end
225 |
226 | def multipart_upload(url, upload_params, params, file)
227 | req = Typhoeus::Request.new(url, method: :post)
228 | upload_params.each do |k, v|
229 | upload_params[k] = v.to_s if v
230 | end
231 | upload_params['file'] = file
232 | req.options[:body] = upload_params
233 | @multi_request = req
234 | res = req.run
235 | raise ApiError.new("Unexpected error: #{res.body}") if !res.headers['Location']
236 | res.headers['Location']
237 | end
238 |
239 | def upload_file_from_url(endpoint, opts)
240 | asynch = opts.delete('asynch') || opts.delete(:asynch)
241 | ['url', 'name', 'size'].each do |k|
242 | raise "Missing value: #{k}" unless opts[k.to_s] || opts[k.to_sym]
243 | end
244 |
245 | res = post(endpoint, opts)
246 | status_url = res['status_url']
247 | if !status_url
248 | raise ApiError.new("Unexpected error: #{res['message'] || 'no status URL returned'}")
249 | end
250 | status_path = "/" + status_url.split(/\//, 4)[-1]
251 | if asynch
252 | return status_path
253 | else
254 | attachment = nil
255 | while !attachment
256 | res = get(status_path)
257 | if res['upload_status'] == 'errored'
258 | raise ApiError.new(res['message'])
259 | elsif !res['upload_status']
260 | raise ApiError.new("Unexpected response")
261 | end
262 | status_path = res['status_path'] if res['status_path']
263 | attachment = res['attachment']
264 | sleep (defined?(SLEEP_TIME) ? SLEEP_TIME : 5) unless attachment
265 | end
266 | attachment
267 | end
268 | end
269 | end
270 |
271 | class ApiError < StandardError; end
272 |
273 | class ResultSet < Array
274 | def initialize(api, arr)
275 | @api = api
276 | super(arr)
277 | end
278 | attr_accessor :next_endpoint
279 | attr_accessor :link
280 |
281 | def more?
282 | !!next_endpoint
283 | end
284 |
285 | def next_page!
286 | ResultSet.new(@api, []) unless next_endpoint
287 | more = @api.get(next_endpoint)
288 | concat(more)
289 | @next_endpoint = more.next_endpoint
290 | @link = more.link
291 | more
292 | end
293 | end
294 | class PairArray < Array
295 | end
296 | end
297 |
298 |
299 | # TODO: this is a hack that digs into the bowels of typhoeus
300 | module Ethon
301 | class Easy
302 | module Queryable
303 | def recursively_generate_pairs(h, prefix, pairs)
304 | case h
305 | when Hash
306 | h.each_pair do |k,v|
307 | key = prefix.nil? ? k : "#{prefix}[#{k}]"
308 | pairs_for(v, key, pairs)
309 | end
310 | when Canvas::PairArray
311 | h.each do |k, v|
312 | key = prefix.nil? ? k : "#{prefix}[#{k}]"
313 | pairs_for(v, key, pairs)
314 | end
315 | when Array
316 | h.each_with_index do |v, i|
317 | key = "#{prefix}[#{i}]"
318 | pairs_for(v, key, pairs)
319 | end
320 | end
321 | end
322 | end
323 | end
324 | end
--------------------------------------------------------------------------------
/spec/custom_ids_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "Custom IDs" do
4 | it "should hex encode values correctly" do
5 | Canvas::API.encode_id('sis_course_id', '12344').should == 'hex:sis_course_id:3132333434'
6 | Canvas::API.encode_id('sis_user_id', 8900).should == 'hex:sis_user_id:38393030'
7 | Canvas::API.encode_id('sis_course_id', 'A)*#$B^)M)_@$*^B$_V@_#%*@#_').should == 'hex:sis_course_id:41292a2324425e294d295f40242a5e42245f56405f23252a40235f'
8 | end
9 |
10 | it "should fail gracefully on bad inputs" do
11 | Canvas::API.encode_id(nil, nil).should == nil
12 | Canvas::API.encode_id('sis_course_id', nil).should == nil
13 | Canvas::API.encode_id(nil, '12345').should == nil
14 | end
15 | end
--------------------------------------------------------------------------------
/spec/file_uploads_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "File Uploads" do
4 | context "from local" do
5 | it "should fail on invalid file object" do
6 | token_api
7 | expect { @api.upload_file_from_local("/api/v1/users/self/files", nil) }.to raise_error("Missing File object")
8 | end
9 |
10 | it "should make a valid setup call" do
11 | token_api
12 | file = file_handle
13 | args = {
14 | :size => file.size,
15 | :name => 'canvas-api.rb',
16 | :content_type => 'application/octet-stream',
17 | :on_duplicate => nil
18 | }
19 | @api.should_receive(:post).with("/api/v1/users/self/files", args).and_return({})
20 | expect { @api.upload_file_from_local("/api/v1/users/self/files", file) }.to raise_error("Unexpected error: no upload URL returned")
21 | end
22 |
23 | it "should call multipart_upload correctly" do
24 | token_api
25 | file = file_handle
26 | args = {
27 | :size => file.size,
28 | :name => 'canvas-api.rb',
29 | :content_type => 'application/octet-stream',
30 | :on_duplicate => nil
31 | }
32 | @api.should_receive(:post).with("/api/v1/users/self/files", args).and_return({'upload_url' => 'http://www.bacon.com', 'upload_params' => {'a' => 1, 'b' => 2}})
33 | @api.should_receive(:multipart_upload).and_raise("stop at multipart") #return("http://www.return.url/api/v1/success")
34 | expect { @api.upload_file_from_local("/api/v1/users/self/files", file) }.to raise_error("stop at multipart")
35 | end
36 |
37 | context "multipart_upload" do
38 | it "should error on missing status URL return" do
39 | token_api
40 | file = file_handle
41 | Typhoeus::Request.any_instance.should_receive(:run).and_return(OpenStruct.new({'headers' => {}, 'body' => 'nothing good here'}))
42 | expect {
43 | @api.multipart_upload("http://www.example.com/", {'a' => 1, 'b' => 2}, {:content_type => 'application/octet-stream', :name => 'file'}, file)
44 | }.to raise_error(Canvas::ApiError, "Unexpected error: nothing good here")
45 | end
46 |
47 | it "should upload the parameters in the correct order (file last)" do
48 | path_to_lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
49 | token_api
50 | file = file_handle
51 | Typhoeus::Request.any_instance.should_receive(:run).and_return(OpenStruct.new({'headers' => {'Location' => 'http://www.new_status.url/api/v1/success'}}))
52 | res = @api.multipart_upload("http://www.example.com/", {'a' => 1, 'b' => 2}, {:content_type => 'application/octet-stream', :name => 'file'}, file)
53 | res.should == 'http://www.new_status.url/api/v1/success'
54 | req = @api.instance_variable_get('@multi_request')
55 | req.encoded_body.split(/&/)[-1].should == "file=canvas-api.rb=application/octet-stream=#{path_to_lib}/canvas-api.rb"
56 | end
57 | end
58 |
59 | it "should call the final status URL after upload" do
60 | token_api
61 | file = file_handle
62 | args = {
63 | :size => file.size,
64 | :name => 'canvas-api.rb',
65 | :content_type => 'application/octet-stream',
66 | :on_duplicate => nil
67 | }
68 | @api.should_receive(:post).with("/api/v1/users/self/files", args).and_return({'upload_url' => 'http://www.bacon.com', 'upload_params' => {'a' => 1, 'b' => 2}})
69 | @api.should_receive(:multipart_upload).and_return("http://www.return.url/api/v1/success")
70 | @api.should_receive(:get).with("/api/v1/success").and_return({'success' => true})
71 | res = @api.upload_file_from_local("/api/v1/users/self/files", file)
72 | res.should == {'success' => true}
73 | end
74 | end
75 |
76 | context "from URL" do
77 | it "should fail on missing values" do
78 | token_api
79 | expect { @api.upload_file_from_url("/api/v1/users/self/files", {}) }.to raise_error("Missing value: url")
80 | expect { @api.upload_file_from_url("/api/v1/users/self/files", :url => "http://www.example.com") }.to raise_error("Missing value: name")
81 | expect { @api.upload_file_from_url("/api/v1/users/self/files", :url => "http://www.example.com", :name => "file.html") }.to raise_error("Missing value: size")
82 | end
83 |
84 | it "should retrieve the status PATH (not URL) on valid setup step" do
85 | token_api
86 | args = {:url => "http://www.example.com", :name => "file.html", :size => 10}
87 | @api.should_receive(:post).with("/api/v1/users/self/files", args).and_return({'status_url' => 'http://www.bob.com/bob'})
88 | path = @api.upload_file_from_url("/api/v1/users/self/files", args.merge(:asynch => true))
89 | path.should == '/bob'
90 | end
91 |
92 | it "should raise an error on problems with the setup step" do
93 | token_api
94 | args = {:url => "http://www.example.com", :name => "file.html", :size => 10}
95 | @api.should_receive(:post).with("/api/v1/users/self/files", args).and_return({})
96 | expect { @api.upload_file_from_url("/api/v1/users/self/files", args.merge(:asynch => true)) }.to raise_error(Canvas::ApiError, "Unexpected error: no status URL returned")
97 | end
98 |
99 | it "should repeatedly call the status PATH on non-asynch" do
100 | token_api
101 | args = {:url => "http://www.example.com", :name => "file.html", :size => 10}
102 | @api.should_receive(:post).with("/api/v1/users/self/files", args).and_return({'status_url' => 'http://www.bob.com/bob'})
103 | @api.should_receive(:get).with("/bob").and_return({'upload_status' => 'pending', 'status_path' => '/bob2'})
104 | @api.should_receive(:get).with("/bob2").and_return({'upload_status' => 'pending', 'status_path' => '/bob3'})
105 | @api.should_receive(:get).with("/bob3").and_return({'upload_status' => 'success', 'attachment' => {'id' => 2}})
106 | attachment = @api.upload_file_from_url("/api/v1/users/self/files", args)
107 | attachment['id'].should == 2
108 | end
109 |
110 | it "should raise an error on an errored non-asynch call" do
111 | token_api
112 | args = {:url => "http://www.example.com", :name => "file.html", :size => 10}
113 | @api.should_receive(:post).with("/api/v1/users/self/files", args).and_return({'status_url' => 'http://www.bob.com/bob'})
114 | @api.should_receive(:get).with("/bob").and_return({'upload_status' => 'errored', 'message' => 'bad robot'})
115 | expect { @api.upload_file_from_url("/api/v1/users/self/files", args) }.to raise_error(Canvas::ApiError, "bad robot")
116 | end
117 | end
118 | end
119 |
--------------------------------------------------------------------------------
/spec/get_requests_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "GET requests" do
4 | context "validate_call" do
5 | it "should raise on missing token" do
6 | client_api
7 | expect { @api.validate_call(nil) }.to raise_error(StandardError, "token required for api calls")
8 | end
9 |
10 | it "should raise on missing host" do
11 | token_api
12 | @api.host = nil
13 | expect { @api.validate_call(nil) }.to raise_error(StandardError, "missing host")
14 | end
15 |
16 | it "should raise on invalid endpoint" do
17 | token_api
18 | expect { @api.validate_call(nil) }.to raise_error(StandardError, "missing endpoint")
19 | expect { @api.validate_call("api/v1/bacon") }.to raise_error(StandardError, "missing leading slash on endpoint")
20 | end
21 |
22 | it "should raise on malformed endpoint URL" do
23 | token_api
24 | expect { @api.validate_call("/api/v1/A$#^B$^#$B^") }.to raise_error(StandardError, "invalid endpoint")
25 | end
26 | end
27 |
28 | context "generate_uri" do
29 | it "should correctly append the access token" do
30 | token_api
31 | @api.generate_uri("/api/v1/bacon").to_s.should == "http://canvas.example.com/api/v1/bacon?access_token=#{@api.token}"
32 | end
33 |
34 | it "should correctly append masquerading id if any set" do
35 | token_api
36 | @api.masquerade_as(123)
37 | @api.generate_uri("/api/v1/bacon?a=1").to_s.should == "http://canvas.example.com/api/v1/bacon?a=1&access_token=#{@api.token}&as_user_id=123"
38 | end
39 |
40 | it "should generate a valid http object" do
41 | token_api
42 | @api.instance_variable_get('@http').should == nil
43 | @api.generate_uri("/api/v1/bacon")
44 | http = @api.instance_variable_get('@http')
45 | http.should_not == nil
46 | end
47 |
48 | it "should correctly detect ssl" do
49 | token_api
50 | @api.instance_variable_get('@http').should == nil
51 | @api.generate_uri("/api/v1/bacon")
52 | http = @api.instance_variable_get('@http')
53 | http.should_not == nil
54 | http.use_ssl?.should == false
55 |
56 | @api.host = "https://canvas.example.com"
57 | @api.generate_uri("/api/v1/bacon")
58 | http = @api.instance_variable_get('@http')
59 | http.should_not == nil
60 | http.use_ssl?.should == true
61 | end
62 | end
63 |
64 | context "retrieve_response" do
65 | it "should raise on redirect" do
66 | token_api
67 | @api.generate_uri("/api/v1/bacon")
68 | stub_request("/api/v1/bacon", :code => 302, :location => "http://www.example.com")
69 | req = @api.get_request("/api/v1/bacon")
70 | expect { @api.retrieve_response(req) }.to raise_error(Canvas::ApiError, "unexpected redirect to http://www.example.com")
71 | end
72 |
73 | it "should raise on non-200 response" do
74 | token_api
75 | @api.generate_uri("/api/v1/bacon")
76 | stub_request("/api/v1/bacon", :code => 400, :body => {}.to_json)
77 | req = @api.get_request("/api/v1/bacon")
78 | expect { @api.retrieve_response(req) }.to raise_error(Canvas::ApiError, "400 unexpected error")
79 | end
80 |
81 | it "should parse error messages" do
82 | token_api
83 | @api.generate_uri("/api/v1/bacon")
84 | stub_request("/api/v1/bacon", :code => 400, :body => {:message => "bad message", :status => "invalid"}.to_json)
85 | req = @api.get_request("/api/v1/bacon")
86 | expect { @api.retrieve_response(req) }.to raise_error(Canvas::ApiError, "invalid bad message")
87 | end
88 |
89 | it "should raise on non-JSON response" do
90 | token_api
91 | @api.generate_uri("/api/v1/bacon")
92 | stub_request("/api/v1/bacon", :code => 400, :body => "")
93 | req = @api.get_request("/api/v1/bacon")
94 | expect { @api.retrieve_response(req) }.to raise_error(Canvas::ApiError, "invalid JSON")
95 | end
96 |
97 | it "should return JSON on valid response" do
98 | token_api
99 | @api.generate_uri("/api/v1/bacon")
100 | stub_request("/api/v1/bacon", :code => 200, :body => {:bacon => true}.to_json)
101 | req = @api.get_request("/api/v1/bacon")
102 | json = @api.retrieve_response(req)
103 | json['bacon'].should == true
104 | end
105 |
106 | it "should return ResultSet on valid array response" do
107 | token_api
108 | @api.generate_uri("/api/v1/bacon")
109 | stub_request("/api/v1/bacon", :code => 200, :body => [{:bacon => true}].to_json)
110 | req = @api.get_request("/api/v1/bacon")
111 | json = @api.retrieve_response(req)
112 | json[0]['bacon'].should == true
113 | json.next_endpoint.should == nil
114 | end
115 |
116 | it "should append query parameters if specified" do
117 | token_api
118 | @api.generate_uri("/api/v1/bacon?c=x", {'a' => '1', 'b' => '2'}).request_uri.should == "/api/v1/bacon?c=x&access_token=#{@api.token}&a=1&b=2"
119 | @api.should_receive(:generate_uri).with('/api/v1/bob', {'a' => 1})
120 | @api.should_receive(:get_request).and_raise("stop here")
121 | expect { @api.get("/api/v1/bob", {'a' => 1}) }.to raise_error("stop here")
122 | end
123 |
124 | it "should handle numerical query parameters" do
125 | token_api
126 | @api.generate_uri("/api/v1/bacon", {'a' => 1, 'b' => 2, 'c' => @api}).request_uri.should == "/api/v1/bacon?access_token=#{@api.token}&a=1&b=2&c=#{CGI.escape(@api.to_s)}"
127 | end
128 |
129 | it "should handle array query parameters, with or without the []" do
130 | token_api
131 | @api.generate_uri("/api/v1/bacon", {'a[]' => 1, 'b' => [2,3]}).request_uri.should == "/api/v1/bacon?access_token=#{@api.token}&a%5B%5D=1&b%5B%5D=2&b%5B%5D=3"
132 | @api.generate_uri("/api/v1/bacon", [['a[]', 1], ['b', [2,3]]]).request_uri.should == "/api/v1/bacon?access_token=#{@api.token}&a%5B%5D=1&b%5B%5D=2&b%5B%5D=3"
133 | end
134 |
135 | it "should not fail on no query parameters argument" do
136 | token_api
137 | @api.generate_uri("/api/v1/bacon").request_uri.should == "/api/v1/bacon?access_token=#{@api.token}"
138 | end
139 | end
140 | end
141 |
--------------------------------------------------------------------------------
/spec/init_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "Init" do
4 | it "should error if no host provided" do
5 | expect { Canvas::API.new }.to raise_error(RuntimeError, "host required")
6 | expect { Canvas::API.new(:host => nil) }.to raise_error(RuntimeError, "host required")
7 | end
8 |
9 | it "should error if invalid host provided" do
10 | expect { Canvas::API.new(:host => "canvas.example.com") }.to raise_error(RuntimeError, "invalid host, protocol required")
11 | expect { Canvas::API.new(:host => "ftp://canvas.example.com") }.to raise_error(RuntimeError, "invalid host, protocol required")
12 | expect { Canvas::API.new(:host => "http://canvas.example.com/") }.to raise_error(RuntimeError, "invalid host")
13 | expect { Canvas::API.new(:host => "http://canvas.example.com/") }.to raise_error(RuntimeError, "invalid host")
14 | end
15 |
16 | it "should error if no token or client id provided" do
17 | expect { Canvas::API.new(:host => "http://canvas.example.com") }.to raise_error(RuntimeError, "token or client_id required")
18 | expect { Canvas::API.new(:host => "http://canvas.example.com", :token => nil) }.to raise_error(RuntimeError, "token or client_id required")
19 | end
20 |
21 | it "should accept valid configurations" do
22 | expect { Canvas::API.new(:host => "http://canvas.example.com", :token => "abc") }.to_not raise_error
23 | expect { Canvas::API.new(:host => "http://canvas.api.of.coolness.example.com", :client_id => 123) }.to raise_error(RuntimeError, "secret required for client_id configuration")
24 | expect { Canvas::API.new(:host => "http://canvas.api.of.coolness.example.com", :client_id => 123, :secret => "abc") }.to_not raise_error
25 | end
26 |
27 | end
--------------------------------------------------------------------------------
/spec/masquerading_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "Masquerading" do
4 | it "should remember masquerading id" do
5 | token_api
6 | @api.masquerade_as(2)
7 | @api.instance_variable_get('@as_user_id').should == '2'
8 | end
9 |
10 | it "should use masquerading id when set" do
11 | token_api
12 | @api.masquerade_as(2)
13 | url_called(@api, :get, "/api/v1/bacon").should match(/as_user_id=2/)
14 | url_called(@api, :get, "/api/v1/cheetos").should match(/as_user_id=2/)
15 | end
16 |
17 | it "should stop using masquerading id when cleared" do
18 | token_api
19 | @api.masquerade_as(2)
20 | url_called(@api, :get, "/api/v1/bacon").should match(/as_user_id=2/)
21 | @api.stop_masquerading
22 | url_called(@api, :get, "/api/v1/cheetos").should_not match(/as_user_id/)
23 | end
24 |
25 | it "should ignore nil masquerading id" do
26 | token_api
27 | @api.masquerade_as(nil)
28 | url_called(@api, :get, "/api/v1/bacon").should_not match(/as_user_id/)
29 | end
30 | end
--------------------------------------------------------------------------------
/spec/non_get_requests_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "Non-GET Requests" do
4 | it "should raise on missing token" do
5 | client_api
6 | expect { @api.post(nil) }.to raise_error(StandardError, "token required for api calls")
7 | end
8 |
9 | it "should raise on missing host" do
10 | token_api
11 | @api.host = nil
12 | expect { @api.delete(nil) }.to raise_error(StandardError, "missing host")
13 | end
14 |
15 | it "should raise on invalid endpoint" do
16 | token_api
17 | expect { @api.put(nil) }.to raise_error(StandardError, "missing endpoint")
18 | expect { @api.delete("api/v1/bacon") }.to raise_error(StandardError, "missing leading slash on endpoint")
19 | end
20 |
21 | it "should raise on malformed endpoint URL" do
22 | token_api
23 | expect { @api.put("/api/v1/A$#^B$^#$B^") }.to raise_error(StandardError, "invalid endpoint")
24 | end
25 |
26 | context "clean_params" do
27 |
28 | it "should fix hashed parameters list" do
29 | token_api
30 | @api.clean_params({}).should == []
31 | @api.clean_params({'a' => 1, 'b' => 2}).should == [['a', '1'],['b','2']]
32 | @api.clean_params({'a' => {'a' => 1, 'b' => 3}, 'b' => 2}).should == [["a[a]", "1"], ["a[b]", "3"], ["b", "2"]]
33 |
34 | a = @api.clean_params({'user[name]' => 'Dixon Wilson', 'user[short_name]' => 'Dixon'})
35 | b = @api.clean_params({'user' => {'name' => 'Dixon Wilson', 'short_name' => 'Dixon'}})
36 | a.should == b
37 | c = @api.clean_params([['user[name]', 'Dixon Wilson'],['user[short_name]', 'Dixon']])
38 | a.should == c
39 | b.should == c
40 | end
41 |
42 | it "should support arbitrary levels of nesting on hashed parameters list" do
43 | token_api
44 | @api.clean_params({'a' => 1, 'b' => 2}).should == [['a', '1'],['b','2']]
45 | @api.clean_params({'a' => {'b' => {'c' => {'d' => 1}}}}).should == [["a[b][c][d]", "1"]]
46 | end
47 |
48 | it "should fail on arrays for hashed parameters list" do
49 | token_api
50 | expect { @api.clean_params({'a' => [1,2]}) }.to raise_error("No support for nested array parameters currently")
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/oauth_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "OAuth" do
4 | context "oauth_url" do
5 | # including login_url
6 | it "should fail without a client_id and secret" do
7 | token_api
8 | expect { @api.oauth_url(nil) }.to raise_error(StandardError, "client_id required for oauth flow")
9 | @api.client_id = "bob"
10 | expect { @api.oauth_url(nil) }.to raise_error(StandardError, "secret required for oauth flow")
11 | end
12 |
13 | it "should fail gracefully on an invalid callback_url" do
14 | client_api
15 | expect { @api.oauth_url(nil) }.to raise_error(StandardError, "callback_url required")
16 | expect { @api.oauth_url("(*&^%$#") }.to raise_error(StandardError, "invalid callback_url")
17 | end
18 |
19 | it "should craft valie urls" do
20 | client_api
21 | @api.oauth_url("http://www.example.com", nil).should == "http://canvas.example.com/login/oauth2/auth?client_id=#{@api.client_id}&response_type=code&redirect_uri=http%3A%2F%2Fwww.example.com"
22 | @api.oauth_url("http://www.example.com/return?id=1234", "cool/scope").should == "http://canvas.example.com/login/oauth2/auth?client_id=#{@api.client_id}&response_type=code&redirect_uri=http%3A%2F%2Fwww.example.com%2Freturn%3Fid%3D1234&scopes=cool%2Fscope"
23 | @api.login_url("http://www.example.com").should == "http://canvas.example.com/login/oauth2/auth?client_id=#{@api.client_id}&response_type=code&redirect_uri=http%3A%2F%2Fwww.example.com&scopes=%2Fauth%2Fuserinfo"
24 | end
25 | end
26 |
27 | context "retrieve_access_token" do
28 | it "should fail without a client_id and secret" do
29 | token_api
30 | expect { @api.retrieve_access_token(nil, nil) }.to raise_error(StandardError, 'client_id required for oauth flow')
31 | end
32 |
33 | it "should fail without a code" do
34 | client_api
35 | expect { @api.retrieve_access_token(nil, nil) }.to raise_error(StandardError, 'code required')
36 | end
37 |
38 | it "should fail on an invalid callback_url" do
39 | client_api
40 | expect { @api.retrieve_access_token("abc", nil) }.to raise_error(StandardError, 'callback_url required')
41 | expect { @api.retrieve_access_token("abc", "(*&^%$#") }.to raise_error(StandardError, 'invalid callback_url')
42 | end
43 |
44 | it "should successfully retrieve an access token" do
45 | client_api
46 | @api.should_receive(:post).and_return({'access_token' => 'asdf'})
47 | res = @api.retrieve_access_token("abc", "http://www.example.com")
48 | res.should == {'access_token' => 'asdf'}
49 | @api.token.should == 'asdf'
50 | end
51 | end
52 |
53 | context "logout" do
54 | it "should fail without a token" do
55 | client_api
56 | expect { @api.logout }.to raise_error(StandardError, "token required for api calls")
57 | end
58 |
59 | it "should return success" do
60 | token_api
61 | @api.should_receive(:delete).and_return({'logged_out' => true})
62 | @api.logout.should == true
63 |
64 | @api.should_receive(:delete).and_return({'logged_out' => false})
65 | @api.logout.should == false
66 |
67 | @api.should_receive(:delete).and_return({})
68 | @api.logout.should == false
69 | end
70 | end
71 | end
72 |
73 | # def retrieve_access_token(code, callback_url)
74 | # raise "client_id required for oauth flow" unless @client_id
75 | # raise "secret required for oauth flow" unless @secret
76 | # post("/login/oauth2/token", :client_id => @client_id, :redirect_uri => callback_url, :client_secret => @secret, :code => code)
77 | # end
78 | #
79 | # def logout
80 | # delete("/login/oauth2/token")
81 | # end
82 | #
83 |
--------------------------------------------------------------------------------
/spec/pagination_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "Pagination" do
4 | it "should return an array of results on a valid call" do
5 | token_api
6 | @api.generate_uri("/api/v1/bacon")
7 | stub_request("/api/v1/bacon", :code => 200, :body => [{:bacon => true}].to_json)
8 | req = @api.get_request("/api/v1/bacon")
9 | json = @api.retrieve_response(req)
10 | json.should be_is_a(Array)
11 | end
12 |
13 | it "should return a Canvas result set" do
14 | token_api
15 | @api.generate_uri("/api/v1/bacon")
16 | stub_request("/api/v1/bacon", :code => 200, :body => [{:bacon => true}].to_json)
17 | req = @api.get_request("/api/v1/bacon")
18 | json = @api.retrieve_response(req)
19 | json.should be_is_a(Canvas::ResultSet)
20 | end
21 |
22 | context "next_page!" do
23 | it "should correctly retrieve the next set of results" do
24 | token_api
25 | @api.generate_uri("/api/v1/bacon")
26 | stub_request("/api/v1/bacon", :code => 200, :body => [{:bacon => true}].to_json, :link => "; rel=\"next\",; rel=\"first\",; rel=\"last\"")
27 | req = @api.get_request("/api/v1/bacon")
28 | json = @api.retrieve_response(req)
29 | json.should be_is_a(Array)
30 | json.length.should == 1
31 | json.next_endpoint.should == "/api/v1/bacon?page=2"
32 |
33 | stub_request("/api/v1/bacon?page=2", :code => 200, :body => [{:bacon => true}].to_json)
34 | new_list = json.next_page!
35 | json.length.should == 2
36 | new_list.length.should == 1
37 | json.next_endpoint.should == nil
38 | end
39 |
40 | it "should correctly retrieve more than two pages" do
41 | token_api
42 | @api.generate_uri("/api/v1/bacon")
43 | stub_request("/api/v1/bacon", :code => 200, :body => [{:bacon => true}].to_json, :link => "; rel=\"next\",; rel=\"first\",; rel=\"last\"")
44 | req = @api.get_request("/api/v1/bacon")
45 | json = @api.retrieve_response(req)
46 | json.should be_is_a(Array)
47 | json.length.should == 1
48 | json.next_endpoint.should == "/api/v1/bacon?page=2"
49 |
50 | stub_request("/api/v1/bacon?page=2", :code => 200, :body => [{:bacon => true}].to_json, :link => "; rel=\"next\",; rel=\"first\",; rel=\"last\"")
51 | new_list = json.next_page!
52 | json.length.should == 2
53 | new_list.length.should == 1
54 | json.next_endpoint.should == "/api/v1/bacon?page=3"
55 |
56 | stub_request("/api/v1/bacon?page=3", :code => 200, :body => [{:bacon => true}].to_json, :link => "; rel=\"first\",; rel=\"last\"")
57 | new_list = json.next_page!
58 | json.length.should == 3
59 | new_list.length.should == 1
60 | json.next_endpoint.should == nil
61 | end
62 |
63 | it "should correctly handle when there are no more pages" do
64 | token_api
65 | @api.generate_uri("/api/v1/bacon")
66 | stub_request("/api/v1/bacon", :code => 200, :body => [{:bacon => true}].to_json, :link => "; rel=\"next\",; rel=\"first\",; rel=\"last\"")
67 | req = @api.get_request("/api/v1/bacon")
68 | json = @api.retrieve_response(req)
69 | json.should be_is_a(Array)
70 | json.length.should == 1
71 | json.next_endpoint.should == "/api/v1/bacon?page=2"
72 |
73 | stub_request("/api/v1/bacon?page=2", :code => 200, :body => [].to_json, :link => "; rel=\"next\",; rel=\"first\",; rel=\"last\"")
74 | new_list = json.next_page!
75 | json.length.should == 1
76 | new_list.length.should == 0
77 | json.next_endpoint.should == "/api/v1/bacon?page=3"
78 |
79 | stub_request("/api/v1/bacon?page=3", :code => 200, :body => [].to_json, :link => "; rel=\"first\",; rel=\"last\"")
80 | new_list = json.next_page!
81 | json.length.should == 1
82 | new_list.length.should == 0
83 | json.next_endpoint.should == nil
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
3 | SLEEP_TIME = 0.1
4 |
5 | require 'canvas-api'
6 | require 'rspec'
7 | require 'net/http'
8 | require 'ostruct'
9 |
10 | def token_api
11 | @api = Canvas::API.new(:host => "http://canvas.example.com", :token => "abc#{rand(999)}")
12 | end
13 |
14 | def client_api
15 | @api = Canvas::API.new(:host => "http://canvas.example.com", :client_id => rand(99999), :secret => rand(99999).to_s)
16 | end
17 |
18 | def file_handle
19 | File.open(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'canvas-api.rb')), 'r')
20 | end
21 |
22 | def url_called(api, *args)
23 | @api.should_receive(:retrieve_response).and_return(nil)
24 | @api.send(*args)
25 | @api.instance_variable_get('@uri').to_s
26 | end
27 |
28 | def stub_request(endpoint, args)
29 | @request ||= Typhoeus::Request.new(endpoint)
30 | @response ||= OpenStruct.new
31 | @response.code = (args[:code] || 200).to_s
32 | @response.body = args[:body].to_s
33 | @response.headers = {
34 | 'Location' => args[:location],
35 | 'Link' => args[:link]
36 | }
37 |
38 | Typhoeus::Request.any_instance.stub(:run).and_return(@response)
39 | @api.should_receive(:get_request).and_return(@request)
40 | @request
41 | end
42 |
--------------------------------------------------------------------------------