├── .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 | --------------------------------------------------------------------------------