# # Copyright (C) 2011 Instructure, Inc. # # This file is part of Canvas. # # Canvas is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, version 3 of the License. # # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . # require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') require 'nokogiri' RSpec::configure do |c| # rspec-rails 3 will no longer automatically infer an example group's spec type # from the file location. You can explicitly opt-in to the feature using this # config option. # To explicitly tag specs without using automatic inference, set the `:type` # metadata manually: # # describe ThingsController, :type => :controller do # # Equivalent to being in spec/controllers # end c.infer_spec_type_from_file_location! end class HashWithDupCheck < Hash def []=(k,v) if self.key?(k) raise ArgumentError, "key already exists: #{k.inspect}" else super end end end # make an API call using the given method (GET/PUT/POST/DELETE), # to the given path (e.g. /api/v1/courses). params will be verified to match the # params generated by the Rails routing engine. body_params are params in a # PUT/POST that are included in the body rather than the URI, and therefore # don't affect routing. def api_call(method, path, params, body_params = {}, headers = {}, opts = {}) raw_api_call(method, path, params, body_params, headers, opts) if opts[:expected_status] assert_status(opts[:expected_status]) else unless response.success? error_message = response.body begin json = JSON.parse(response.body) error_report_id = json['error_report_id'] error_report = ErrorReport.find_by(id: error_report_id) if error_report_id if error_report error_message << "\n" error_message << error_report.message error_message << "\n" error_message << error_report.backtrace end rescue JSON::ParserError end end expect(response).to be_success, error_message end if response.headers['Link'] # make sure that the link header is properly formed Api.parse_pagination_links(response.headers['Link']) end if jsonapi_call?(headers) && method == :delete assert_status(204) return end case params[:format] when 'json' expect(response.header[content_type_key]).to eq 'application/json; charset=utf-8' body = response.body if body.respond_to?(:call) StringIO.new.tap { |sio| body.call(nil, sio); body = sio.string } end body.sub!(%r{^while\(1\);}, '') # Check that the body doesn't have any duplicate keys. this can happen if # you add both a string and a symbol to the hash before calling to_json on # it. # The ruby JSON gem allows this, and it's technically valid JSON to have # duplicate names in an object ("names SHOULD be unique"), but it's silly # and we're not gonna let it slip through again. JSON.parse(body, :object_class => HashWithDupCheck) else raise("Don't know how to handle response format #{params[:format]}") end end def jsonapi_call?(headers) headers['Accept'] == 'application/vnd.api+json' end # like api_call, but performed by the specified user instead of @user def api_call_as_user(user, method, path, params, body_params = {}, headers = {}, opts = {}) token = access_token_for_user(user) headers['Authorization'] = "Bearer #{token}" account = opts[:domain_root_account] || Account.default user.pseudonyms.reload account.pseudonyms.create!(:unique_id => "#{user.id}@example.com", :user => user) unless user.find_pseudonym_for_account(account, true) Pseudonym.any_instance.stubs(:works_for_account?).returns(true) api_call(method, path, params, body_params, headers, opts) end $spec_api_tokens = {} def access_token_for_user(user) token = $spec_api_tokens[user] unless token token = $spec_api_tokens[user] = user.access_tokens.create!(:purpose => "test").full_token end token end # like api_call, but don't assume success and a json response. def raw_api_call(method, path, params, body_params = {}, headers = {}, opts = {}) path = path.sub(%r{\Ahttps?://[^/]+}, '') # remove protocol+host enable_forgery_protection do params_from_with_nesting(method, path).each do |key, value| expect(params[key].to_s).to eq(value.to_s), lambda{ "Expected value of params[\'#{key}\'] to equal #{value}, actual: #{params[key]}"} end if @use_basic_auth user_session(@user) else headers['HTTP_AUTHORIZATION'] = headers['Authorization'] if headers.key?('Authorization') if !params.key?(:api_key) && !params.key?(:access_token) && !headers.key?('HTTP_AUTHORIZATION') && @user token = access_token_for_user(@user) headers['HTTP_AUTHORIZATION'] = "Bearer #{token}" account = opts[:domain_root_account] || Account.default Pseudonym.any_instance.stubs(:works_for_account?).returns(true) account.pseudonyms.create!(:unique_id => "#{@user.id}@example.com", :user => @user) unless @user.all_active_pseudonyms(:reload) && @user.find_pseudonym_for_account(account, true) end end LoadAccount.stubs(:default_domain_root_account).returns(opts[:domain_root_account]) if opts.has_key?(:domain_root_account) __send__(method, path, params.reject { |k,v| %w(controller action).include?(k.to_s) }.merge(body_params), headers) end end def follow_pagination_link(rel, params={}) links = Api.parse_pagination_links(response.headers['Link']) link = links.find{ |l| l[:rel] == rel } link.delete(:rel) uri = link.delete(:uri).to_s link.each{ |key,value| params[key.to_sym] = value } api_call(:get, uri, params) end def params_from_with_nesting(method, path) path, querystring = path.split('?') params = CanvasRails::Application.routes.recognize_path(path, :method => method) querystring.blank? ? params : params.merge(Rack::Utils.parse_nested_query(querystring).symbolize_keys!) end def api_json_response(objects, opts = nil) JSON.parse(objects.to_json(opts.merge(:include_root => false))) end def check_document(html, course, attachment, include_verifiers) doc = Nokogiri::HTML::DocumentFragment.parse(html) img1 = doc.at_css('img#1') expect(img1).to be_present params = include_verifiers ? "?verifier=#{attachment.uuid}" : "" expect(img1['src']).to eq "http://www.example.com/courses/#{course.id}/files/#{attachment.id}/preview#{params}" img2 = doc.at_css('img#2') expect(img2).to be_present expect(img2['src']).to eq "http://www.example.com/courses/#{course.id}/files/#{attachment.id}/download#{params}" video = doc.at_css('video') expect(video).to be_present expect(video['poster']).to match(%r{http://www.example.com/media_objects/qwerty/thumbnail}) expect(video['src']).to match(%r{http://www.example.com/courses/#{course.id}/media_download}) expect(video['src']).to match(%r{entryId=qwerty}) expect(doc.css('a').last['data-api-endpoint']).to match(%r{http://www.example.com/api/v1/courses/#{course.id}/pages/awesome-page}) expect(doc.css('a').last['data-api-returntype']).to eq 'Page' end # passes the cb a piece of user content html text. the block should return the # response from the api for that field, which will be verified for correctness. def should_translate_user_content(course, include_verifiers=true) attachment = attachment_model(:context => course) content = %{

Hello, students.
This will explain everything: important This won't explain anything: important Also, watch this awesome video: And refer to this awesome wiki page.

} html = yield content check_document(html, course, attachment, include_verifiers) if include_verifiers # try again but with cookie auth; shouldn't have verifiers now @use_basic_auth = true html = yield content check_document(html, course, attachment, false) end end def should_process_incoming_user_content(context) attachment_model(:context => context) incoming_content = "

content blahblahblah haha

" saved_content = yield incoming_content expect(saved_content).to eq "

content blahblahblah haha

" end def verify_json_error(error, field, code, message = nil) expect(error["field"]).to eq field expect(error["code"]).to eq code expect(error["message"]).to eq message if message end # Assert the provided JSON hash complies with the JSON-API format specification. # # The following tests will be carried out: # # - all resource entries must be wrapped inside arrays, even if the set # includes only a single resource entry # - when associations are present, a "meta" entry should be present and # it should indicate the primary set in the "primaryCollection" key # # @param [Hash] json # The JSON construct to test. # # @param [String] primary_set # Name of the primary resource the construct represents, i.e, the model # the API endpoint represents, like 'quiz', 'assignment', or 'submission'. # # @param [Array] associations # An optional set of associated resources that should be included with # the primary resource (e.g, a user, an assignment, a submission, etc.). # # @example Testing a Quiz API model: # test_jsonapi_compliance!(json, 'quiz') # # @example Testing a Quiz API model with its assignment included: # test_jsonapi_compliance!(json, 'quiz', [ 'assignment' ]) # # @example A complying construct of a Quiz Submission with its Assignment: # # { # "quiz_submissions": [{ # "id": 10, # "assignment_id": 5 # }], # "assignments": [{ # "id": 5 # }], # "meta": { # "primaryCollection": "quiz_submissions" # } # } # def assert_jsonapi_compliance(json, primary_set, associations = []) required_keys = [ primary_set ] if associations.any? required_keys.concat associations.map { |s| s.pluralize } required_keys << 'meta' end # test key values instead of nr. of keys so we get meaningful failures expect(json.keys.sort).to eq required_keys.sort required_keys.each do |key| expect(json).to be_has_key(key) expect(json[key].is_a?(Array)).to be_truthy unless key == 'meta' end if associations.any? expect(json['meta']['primaryCollection']).to eq primary_set end end