867 lines
40 KiB
Ruby
867 lines
40 KiB
Ruby
#
|
|
# Copyright (C) 2011 - 2013 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
require File.expand_path(File.dirname(__FILE__) + '/../sharding_spec_helper')
|
|
|
|
class TestApiInstance
|
|
include Api
|
|
def initialize(root_account, current_user)
|
|
@domain_root_account = root_account
|
|
@current_user = current_user
|
|
end
|
|
|
|
def account_url(account)
|
|
URI.encode("http://www.example.com/accounts/#{account}")
|
|
end
|
|
|
|
def course_assignment_url(course, assignment)
|
|
URI.encode("http://www.example.com/courses/#{course}/assignments/#{assignment}")
|
|
end
|
|
end
|
|
|
|
describe Api do
|
|
context 'api_find' do
|
|
before do
|
|
@user = user
|
|
@api = TestApiInstance.new Account.default, nil
|
|
end
|
|
|
|
it 'should find a simple record' do
|
|
expect(@user).to eq @api.api_find(User, @user.id)
|
|
end
|
|
|
|
it 'should not find a missing record' do
|
|
expect(lambda {@api.api_find(User, (User.all.map(&:id).max + 1))}).to raise_error(ActiveRecord::RecordNotFound)
|
|
end
|
|
|
|
it 'should find an existing sis_id record' do
|
|
@user = user_with_pseudonym :username => "sis_user_1@example.com"
|
|
expect(@api.api_find(User, "sis_login_id:sis_user_1@example.com")).to eq @user
|
|
end
|
|
|
|
it 'looks for login ids case insensitively' do
|
|
@user = user_with_pseudonym :username => "sis_user_1@example.com"
|
|
expect(@api.api_find(User, "sis_login_id:SIS_USER_1@example.com")).to eq @user
|
|
end
|
|
|
|
it 'properly quotes login ids' do
|
|
@user = user_with_pseudonym :username => "user 'a'"
|
|
expect(@api.api_find(User, "sis_login_id:user 'a'")).to eq @user
|
|
end
|
|
|
|
it 'should not find record from other account' do
|
|
account = Account.create(name: 'new')
|
|
@user = user_with_pseudonym username: "sis_user_1@example.com", account: account
|
|
expect(lambda {@api.api_find(User, "sis_login_id:sis_user_2@example.com")}).to raise_error(ActiveRecord::RecordNotFound)
|
|
end
|
|
|
|
it 'should find record from other root account explicitly' do
|
|
account = Account.create(name: 'new')
|
|
@user = user_with_pseudonym username: "sis_user_1@example.com", account: account
|
|
Api.expects(:sis_parse_id).with("root_account:school:sis_login_id:sis_user_1@example.com", anything, anything).
|
|
returns(['LOWER(pseudonyms.unique_id)', [QuotedValue.new("LOWER('sis_user_1@example.com')"), account]])
|
|
expect(@api.api_find(User, "root_account:school:sis_login_id:sis_user_1@example.com")).to eq @user
|
|
end
|
|
|
|
it 'should allow passing account param and find record' do
|
|
account = Account.create(name: 'new')
|
|
@user = user_with_pseudonym username: "sis_user_1@example.com", account: account
|
|
expect(@api.api_find(User, "sis_login_id:sis_user_1@example.com", account: account)).to eq @user
|
|
end
|
|
|
|
it 'should not find a missing sis_id record' do
|
|
@user = user_with_pseudonym :username => "sis_user_1@example.com"
|
|
expect(lambda {@api.api_find(User, "sis_login_id:sis_user_2@example.com")}).to raise_error(ActiveRecord::RecordNotFound)
|
|
end
|
|
|
|
it 'should find user id "self" when a current user is provided' do
|
|
expect(@user).to eq TestApiInstance.new(Account.default, @user).api_find(User, 'self')
|
|
end
|
|
|
|
it 'should not find user id "self" when a current user is not provided' do
|
|
expect(lambda {TestApiInstance.new(Account.default, nil).api_find(User, "self")}).to raise_error(ActiveRecord::RecordNotFound)
|
|
end
|
|
|
|
it 'should find account id "self"' do
|
|
account = Account.create!
|
|
expect(account).to eq TestApiInstance.new(account, nil).api_find(Account, 'self')
|
|
end
|
|
|
|
it 'should find account id "default"' do
|
|
account = Account.create!
|
|
expect(Account.default).to eq TestApiInstance.new(account, nil).api_find(Account, 'default')
|
|
end
|
|
|
|
it 'should find account id "site_admin"' do
|
|
account = Account.create!
|
|
expect(Account.site_admin).to eq TestApiInstance.new(account, nil).api_find(Account, 'site_admin')
|
|
end
|
|
|
|
it 'should find term id "default"' do
|
|
account = Account.create!
|
|
expect(TestApiInstance.new(account, nil).api_find(account.enrollment_terms, 'default')).to eq account.default_enrollment_term
|
|
end
|
|
|
|
it 'should find term id "current"' do
|
|
account = Account.create!
|
|
term = account.enrollment_terms.create!(start_at: 1.week.ago, end_at: 1.week.from_now)
|
|
expect(TestApiInstance.new(account, nil).api_find(account.enrollment_terms, 'current')).to eq term
|
|
end
|
|
|
|
it 'should not find a "current" term if there is more than one candidate' do
|
|
account = Account.create!
|
|
account.enrollment_terms.create!(start_at: 1.week.ago, end_at: 1.week.from_now)
|
|
account.enrollment_terms.create!(start_at: 2.weeks.ago, end_at: 2.weeks.from_now)
|
|
expect(TestApiInstance.new(account, nil).api_find_all(account.enrollment_terms, ['current'])).to eq []
|
|
end
|
|
|
|
it 'should find an open ended "current" term' do
|
|
account = Account.create!
|
|
term = account.enrollment_terms.create!(start_at: 1.week.ago)
|
|
expect(TestApiInstance.new(account, nil).api_find(account.enrollment_terms, 'current')).to eq term
|
|
end
|
|
|
|
|
|
it 'should not find a user with an invalid AR id' do
|
|
expect(lambda {@api.api_find(User, "a1")}).to raise_error(ActiveRecord::RecordNotFound)
|
|
end
|
|
|
|
it "should not find sis ids in other accounts" do
|
|
account1 = account_model
|
|
account2 = account_model
|
|
api1 = TestApiInstance.new account1, nil
|
|
api2 = TestApiInstance.new account2, nil
|
|
user1 = user_with_pseudonym :username => "sis_user_1@example.com", :account => account1
|
|
user2 = user_with_pseudonym :username => "sis_user_2@example.com", :account => account2
|
|
user3 = user_with_pseudonym :username => "sis_user_3@example.com", :account => account1
|
|
user4 = user_with_pseudonym :username => "sis_user_3@example.com", :account => account2
|
|
expect(api1.api_find(User, "sis_login_id:sis_user_1@example.com")).to eq user1
|
|
expect(lambda {api2.api_find(User, "sis_login_id:sis_user_1@example.com")}).to raise_error(ActiveRecord::RecordNotFound)
|
|
expect(lambda {api1.api_find(User, "sis_login_id:sis_user_2@example.com")}).to raise_error(ActiveRecord::RecordNotFound)
|
|
expect(api2.api_find(User, "sis_login_id:sis_user_2@example.com")).to eq user2
|
|
expect(api1.api_find(User, "sis_login_id:sis_user_3@example.com")).to eq user3
|
|
expect(api2.api_find(User, "sis_login_id:sis_user_3@example.com")).to eq user4
|
|
[user1, user2, user3, user4].each do |user|
|
|
[api1, api2].each do |api|
|
|
expect(api.api_find(User, user.id)).to eq user
|
|
end
|
|
end
|
|
end
|
|
|
|
it "should find user by lti_context_id" do
|
|
@user.lti_context_id = Canvas::Security.hmac_sha1(@user.asset_string.to_s, 'key')
|
|
@user.save!
|
|
expect(@api.api_find(User, "lti_context_id:#{@user.lti_context_id}")).to eq @user
|
|
end
|
|
|
|
it "should find course by lti_context_id" do
|
|
lti_course = course
|
|
lti_course.lti_context_id = Canvas::Security.hmac_sha1(lti_course.asset_string.to_s, 'key')
|
|
lti_course.save!
|
|
expect(@api.api_find(Course, "lti_context_id:#{lti_course.lti_context_id}")).to eq lti_course
|
|
end
|
|
|
|
it "should find account by lti_context_id" do
|
|
account = Account.create!(name: 'account')
|
|
account.lti_context_id = Canvas::Security.hmac_sha1(account.asset_string.to_s, 'key')
|
|
account.save!
|
|
expect(@api.api_find(Account, "lti_context_id:#{account.lti_context_id}")).to eq account
|
|
end
|
|
end
|
|
|
|
context 'api_find_all' do
|
|
before do
|
|
@user = user
|
|
@api = TestApiInstance.new Account.default, nil
|
|
end
|
|
|
|
it 'should find no records' do
|
|
expect(@api.api_find_all(User, [])).to eq []
|
|
end
|
|
|
|
it 'should find a simple record' do
|
|
expect(@api.api_find_all(User, [@user.id])).to eq [@user]
|
|
end
|
|
|
|
it 'should not find a missing record' do
|
|
expect(@api.api_find_all(User, [(User.all.map(&:id).max + 1)])).to eq []
|
|
end
|
|
|
|
it 'should find an existing sis_id record' do
|
|
@user = user_with_pseudonym :username => "sis_user_1@example.com"
|
|
expect(@api.api_find_all(User, ["sis_login_id:sis_user_1@example.com"])).to eq [@user]
|
|
end
|
|
|
|
it 'should find existing records with different lookup strategies' do
|
|
@user1 = user
|
|
@user2 = user_with_pseudonym :username => "sis_user_1@example.com"
|
|
@user3 = user_with_pseudonym
|
|
@pseudonym.sis_user_id = "sis_user_2"
|
|
@pseudonym.save!
|
|
@user4 = user_with_pseudonym :username => "sis_user_3@example.com"
|
|
expect(@api.api_find_all(User, [@user1.id.to_s, "sis_login_id:sis_user_1@example.com", (User.all.map(&:id).max + 1), "sis_user_id:sis_user_2", "sis_login_id:nonexistent@example.com", "sis_login_id:sis_user_3@example.com", "sis_invalid_column:4", "a1"]).sort_by(&:id)).to eq [@user1, @user2, @user3, @user4].sort_by(&:id)
|
|
end
|
|
|
|
it 'should filter out duplicates' do
|
|
@other_user = user
|
|
@user = user_with_pseudonym :username => "sis_user_1@example.com"
|
|
expect(@api.api_find_all(User, [@user.id, "sis_login_id:sis_user_1@example.com", @other_user.id, @user.id]).sort_by(&:id)).to eq [@user, @other_user].sort_by(&:id)
|
|
end
|
|
|
|
it "should find user id 'self' when a current user is provided" do
|
|
@current_user = user
|
|
@other_user = user
|
|
expect(TestApiInstance.new(Account.default, @current_user).api_find_all(User, [@other_user.id, 'self']).sort_by(&:id)).to eq [@current_user, @other_user].sort_by(&:id)
|
|
end
|
|
|
|
it 'should not find user id "self" when a current user is not provided' do
|
|
@current_user = user
|
|
@other_user = user
|
|
expect(TestApiInstance.new(Account.default, nil).api_find_all(User, ["self", @other_user.id])).to eq [@other_user]
|
|
end
|
|
|
|
it "should not find sis ids in other accounts" do
|
|
account1 = account_model
|
|
account2 = account_model
|
|
api1 = TestApiInstance.new account1, nil
|
|
api2 = TestApiInstance.new account2, nil
|
|
user1 = user_with_pseudonym :username => "sis_user_1@example.com", :account => account1
|
|
user2 = user_with_pseudonym :username => "sis_user_2@example.com", :account => account2
|
|
user3 = user_with_pseudonym :username => "sis_user_3@example.com", :account => account1
|
|
user4 = user_with_pseudonym :username => "sis_user_3@example.com", :account => account2
|
|
user5 = user :account => account1
|
|
user6 = user :account => account2
|
|
expect(api1.api_find_all(User, ["sis_login_id:sis_user_1@example.com", "sis_login_id:sis_user_2@example.com", "sis_login_id:sis_user_3@example.com", user5.id, user6.id]).sort_by(&:id)).to eq [user1, user3, user5, user6].sort_by(&:id)
|
|
expect(api2.api_find_all(User, ["sis_login_id:sis_user_1@example.com", "sis_login_id:sis_user_2@example.com", "sis_login_id:sis_user_3@example.com", user5.id, user6.id]).sort_by(&:id)).to eq [user2, user4, user5, user6].sort_by(&:id)
|
|
end
|
|
|
|
it "should not hit the database if no valid conditions were found" do
|
|
collection = mock()
|
|
collection.stubs(:table_name).returns("courses")
|
|
collection.expects(:none).once
|
|
relation = @api.api_find_all(collection, ["sis_invalid:1"])
|
|
expect(relation.to_a).to eq []
|
|
end
|
|
|
|
context "sharding" do
|
|
specs_require_sharding
|
|
|
|
it "should find users from other shards" do
|
|
@shard1.activate { @user2 = User.create! }
|
|
@shard2.activate { @user3 = User.create! }
|
|
|
|
expect(@api.api_find_all(User, [@user2.id, @user3.id]).sort_by(&:global_id)).to eq [@user2, @user3].sort_by(&:global_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'map_ids' do
|
|
it 'should map an empty list' do
|
|
expect(Api.map_ids([], User, Account.default)).to eq []
|
|
end
|
|
|
|
it 'should map a list of AR ids' do
|
|
expect(Api.map_ids([1, 2, '3', '4'], User, Account.default).sort).to eq [1, 2, 3, 4]
|
|
end
|
|
|
|
it "should bail on ids it can't figure out" do
|
|
expect(Api.map_ids(["nonexistentcolumn:5", "sis_nonexistentcolumn:6", 7], User, Account.default)).to eq [7]
|
|
end
|
|
|
|
it "should filter out sis ids that don't exist, but not filter out AR ids" do
|
|
expect(Api.map_ids(["sis_user_id:1", "2"], User, Account.default)).to eq [2]
|
|
end
|
|
|
|
it "should find sis ids that exist" do
|
|
user_with_pseudonym
|
|
@pseudonym.sis_user_id = "sisuser1"
|
|
@pseudonym.save!
|
|
@user1 = @user
|
|
user_with_pseudonym :username => "sisuser2@example.com"
|
|
@user2 = @user
|
|
user_with_pseudonym :username => "sisuser3@example.com"
|
|
@user3 = @user
|
|
expect(Api.map_ids(["sis_user_id:sisuser1", "sis_login_id:sisuser2@example.com",
|
|
"hex:sis_login_id:7369737573657233406578616d706c652e636f6d", "sis_user_id:sisuser4",
|
|
"5123"], User, Account.default).sort).to eq [
|
|
@user1.id, @user2.id, @user3.id, 5123].sort
|
|
end
|
|
|
|
it 'should work when only provided sis_ids' do
|
|
user_with_pseudonym
|
|
@pseudonym.sis_user_id = "sisuser1"
|
|
@pseudonym.save!
|
|
@user1 = @user
|
|
user_with_pseudonym :username => "sisuser2@example.com"
|
|
@user2 = @user
|
|
user_with_pseudonym :username => "sisuser3@example.com"
|
|
@user3 = @user
|
|
expect(Api.map_ids(["sis_user_id:sisuser1", "sis_login_id:sisuser2@example.com",
|
|
"hex:sis_login_id:7369737573657233406578616d706c652e636f6d", "sis_user_id:sisuser4"], User, Account.default).sort).to eq [
|
|
@user1.id, @user2.id, @user3.id].sort
|
|
end
|
|
|
|
it "should not find sis ids in other accounts" do
|
|
account1 = account_model
|
|
account2 = account_model
|
|
user1 = user_with_pseudonym :username => "sisuser1@example.com", :account => account1
|
|
user2 = user_with_pseudonym :username => "sisuser2@example.com", :account => account2
|
|
user3 = user_with_pseudonym :username => "sisuser3@example.com", :account => account1
|
|
user4 = user_with_pseudonym :username => "sisuser3@example.com", :account => account2
|
|
user5 = user :account => account1
|
|
user6 = user :account => account2
|
|
expect(Api.map_ids(["sis_login_id:sisuser1@example.com", "sis_login_id:sisuser2@example.com", "sis_login_id:sisuser3@example.com", user5.id, user6.id], User, account1).sort).to eq [user1.id, user3.id, user5.id, user6.id].sort
|
|
end
|
|
|
|
it 'should try and make params when non-ar_id columns have returned with ar_id columns' do
|
|
collection = mock()
|
|
pluck_result = ["thing2", "thing3"]
|
|
relation_result = mock(eager_load_values: nil, pluck: pluck_result)
|
|
Api.expects(:sis_find_sis_mapping_for_collection).with(collection).returns({:lookups => {"id" => "test-lookup"}})
|
|
Api.expects(:sis_parse_ids).with("test-ids", {"id" => "test-lookup"}, anything).returns({"test-lookup" => ["thing1", "thing2"], "other-lookup" => ["thing2", "thing3"]})
|
|
Api.expects(:relation_for_sis_mapping_and_columns).with(collection, {"other-lookup" => ["thing2", "thing3"]}, {:lookups => {"id" => "test-lookup"}}, "test-root-account").returns(relation_result)
|
|
expect(Api.map_ids("test-ids", collection, "test-root-account")).to eq ["thing1", "thing2", "thing3"]
|
|
end
|
|
|
|
it 'should try and make params when non-ar_id columns have returned without ar_id columns' do
|
|
collection = mock()
|
|
pluck_result = ["thing2", "thing3"]
|
|
relation_result = mock(eager_load_values: nil, pluck: pluck_result)
|
|
Api.expects(:sis_find_sis_mapping_for_collection).with(collection).returns({:lookups => {"id" => "test-lookup"}})
|
|
Api.expects(:sis_parse_ids).with("test-ids", {"id" => "test-lookup"}, anything).returns({"other-lookup" => ["thing2", "thing3"]})
|
|
Api.expects(:relation_for_sis_mapping_and_columns).with(collection, {"other-lookup" => ["thing2", "thing3"]}, {:lookups => {"id" => "test-lookup"}}, "test-root-account").returns(relation_result)
|
|
expect(Api.map_ids("test-ids", collection, "test-root-account")).to eq ["thing2", "thing3"]
|
|
end
|
|
|
|
it 'should not try and make params when no non-ar_id columns have returned with ar_id columns' do
|
|
collection = mock()
|
|
Api.expects(:sis_find_sis_mapping_for_collection).with(collection).returns({:lookups => {"id" => "test-lookup"}})
|
|
Api.expects(:sis_parse_ids).with("test-ids", {"id" => "test-lookup"}, anything).returns({"test-lookup" => ["thing1", "thing2"]})
|
|
Api.expects(:relation_for_sis_mapping_and_columns).never
|
|
expect(Api.map_ids("test-ids", collection, "test-root-account")).to eq ["thing1", "thing2"]
|
|
end
|
|
|
|
it 'should not try and make params when no non-ar_id columns have returned without ar_id columns' do
|
|
collection = mock()
|
|
object1 = mock()
|
|
object2 = mock()
|
|
Api.expects(:sis_find_sis_mapping_for_collection).with(collection).returns({:lookups => {"id" => "test-lookup"}})
|
|
Api.expects(:sis_parse_ids).with("test-ids", {"id" => "test-lookup"}, anything).returns({})
|
|
Api.expects(:sis_make_params_for_sis_mapping_and_columns).at_most(0)
|
|
expect(Api.map_ids("test-ids", collection, "test-root-account")).to eq []
|
|
end
|
|
|
|
end
|
|
|
|
context 'sis_parse_id' do
|
|
before do
|
|
@lookups = Api::SIS_MAPPINGS['users'][:lookups]
|
|
end
|
|
|
|
it 'should handle numeric ids' do
|
|
expect(Api.sis_parse_id(1, @lookups)).to eq ["users.id", 1]
|
|
expect(Api.sis_parse_id(10, @lookups)).to eq ["users.id", 10]
|
|
end
|
|
|
|
it 'should handle numeric ids as strings' do
|
|
expect(Api.sis_parse_id("1", @lookups)).to eq ["users.id", 1]
|
|
expect(Api.sis_parse_id("10", @lookups)).to eq ["users.id", 10]
|
|
end
|
|
|
|
it 'should handle hex_encoded sis_fields' do
|
|
expect(Api.sis_parse_id("hex:sis_login_id:7369737573657233406578616d706c652e636f6d", @lookups)).to eq ["LOWER(pseudonyms.unique_id)", "LOWER('sisuser3@example.com')"]
|
|
expect(Api.sis_parse_id("hex:sis_user_id:7369737573657234406578616d706c652e636f6d", @lookups)).to eq ["pseudonyms.sis_user_id", "sisuser4@example.com"]
|
|
end
|
|
|
|
it 'should not handle invalid hex fields' do
|
|
expect(Api.sis_parse_id("hex:sis_user_id:7369737573657234406578616g706c652e636f6d", @lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("hex:sis_user_id:7369737573657234406578616d06c652e636f6d", @lookups)).to eq [nil, nil]
|
|
end
|
|
|
|
it 'should not handle hex_encoded non-sis fields' do
|
|
expect(Api.sis_parse_id("hex:id:7369737573657233406578616d706c652e636f6d", @lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("hex:1234", @lookups)).to eq [nil, nil]
|
|
end
|
|
|
|
it 'should handle plain sis_fields' do
|
|
expect(Api.sis_parse_id("sis_login_id:sisuser3@example.com", @lookups)).to eq ["LOWER(pseudonyms.unique_id)", "LOWER('sisuser3@example.com')"]
|
|
expect(Api.sis_parse_id("sis_user_id:sisuser4", @lookups)).to eq ["pseudonyms.sis_user_id", "sisuser4"]
|
|
end
|
|
|
|
it "should not handle plain sis_fields that don't exist" do
|
|
expect(Api.sis_parse_id("sis_nonexistent_column:1", @lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_nonexistent:a", @lookups)).to eq [nil, nil]
|
|
end
|
|
|
|
it 'should not handle other things' do
|
|
expect(Api.sis_parse_id("id:1", @lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("a1", @lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("\t10\n11 ", @lookups)).to eq [nil, nil]
|
|
end
|
|
|
|
it 'should handle surrounding whitespace' do
|
|
expect(Api.sis_parse_id("\t10 ", @lookups)).to eq ["users.id", 10]
|
|
expect(Api.sis_parse_id("\t10\n", @lookups)).to eq ["users.id", 10]
|
|
expect(Api.sis_parse_id(" hex:sis_login_id:7369737573657233406578616d706c652e636f6d ", @lookups)).to eq ["LOWER(pseudonyms.unique_id)", "LOWER('sisuser3@example.com')"]
|
|
expect(Api.sis_parse_id(" sis_login_id:sisuser3@example.com\t", @lookups)).to eq ["LOWER(pseudonyms.unique_id)", "LOWER('sisuser3@example.com')"]
|
|
end
|
|
end
|
|
|
|
context 'sis_parse_ids' do
|
|
before do
|
|
@lookups = Api::SIS_MAPPINGS['users'][:lookups]
|
|
end
|
|
|
|
it 'should handle a list of ar_ids' do
|
|
expect(Api.sis_parse_ids([1,2,3], @lookups)).to eq({ "users.id" => [1,2,3] })
|
|
expect(Api.sis_parse_ids(["1","2","3"], @lookups)).to eq({ "users.id" => [1,2,3] })
|
|
end
|
|
|
|
it 'should handle a list of sis ids' do
|
|
expect(Api.sis_parse_ids(["sis_user_id:U1","sis_user_id:U2","sis_user_id:U3"], @lookups)).to eq({ "pseudonyms.sis_user_id" => ["U1","U2","U3"] })
|
|
end
|
|
|
|
it 'should remove duplicates' do
|
|
expect(Api.sis_parse_ids([1,2,3,2], @lookups)).to eq({ "users.id" => [1,2,3] })
|
|
expect(Api.sis_parse_ids([1,2,2,3], @lookups)).to eq({ "users.id" => [1,2,3] })
|
|
expect(Api.sis_parse_ids(["sis_user_id:U1","sis_user_id:U2","sis_user_id:U2","sis_user_id:U3"], @lookups)).to eq({ "pseudonyms.sis_user_id" => ["U1","U2","U3"] })
|
|
expect(Api.sis_parse_ids(["sis_user_id:U1","sis_user_id:U2","sis_user_id:U3","sis_user_id:U2"], @lookups)).to eq({ "pseudonyms.sis_user_id" => ["U1","U2","U3"] })
|
|
end
|
|
|
|
it 'should work with mixed sis id types' do
|
|
expect(Api.sis_parse_ids([1,2,"sis_user_id:U1",3,"sis_user_id:U2","sis_user_id:U3","sis_login_id:A1"], @lookups)).to eq({ "users.id" => [1, 2, 3], "pseudonyms.sis_user_id" => ["U1", "U2", "U3"], "LOWER(pseudonyms.unique_id)" => ["LOWER('A1')"] })
|
|
end
|
|
|
|
it 'should skip invalid things' do
|
|
expect(Api.sis_parse_ids([1,2,3,"a1","invalid","sis_nonexistent:3"], @lookups)).to eq({ "users.id" => [1,2,3] })
|
|
end
|
|
end
|
|
|
|
context 'sis_find_sis_mapping_for_collection' do
|
|
it 'should find the appropriate sis mapping' do
|
|
[Course, EnrollmentTerm, User, Account, CourseSection].each do |collection|
|
|
expect(Api.sis_find_sis_mapping_for_collection(collection)).to eq Api::SIS_MAPPINGS[collection.table_name]
|
|
expect(Api::SIS_MAPPINGS[collection.table_name].is_a?(Hash)).to be_truthy
|
|
end
|
|
end
|
|
|
|
it 'should raise an error otherwise' do
|
|
[StreamItem, PluginSetting].each do |collection|
|
|
expect(lambda {Api.sis_find_sis_mapping_for_collection(collection)}).to raise_error("need to add support for table name: #{collection.table_name}")
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'sis_relation_for_collection' do
|
|
it 'should pass along the sis_mapping to sis_find_params_for_sis_mapping' do
|
|
root_account = account_model
|
|
Api.expects(:relation_for_sis_mapping).with(User, Api::SIS_MAPPINGS['users'], [1,2,3], root_account, anything).returns(1234)
|
|
Api.expects(:sis_find_sis_mapping_for_collection).with(User).returns(Api::SIS_MAPPINGS['users'])
|
|
expect(Api.sis_relation_for_collection(User, [1,2,3], root_account)).to eq 1234
|
|
end
|
|
end
|
|
|
|
context 'relation_for_sis_mapping' do
|
|
it 'should pass along the parsed ids to sis_make_params_for_sis_mapping_and_columns' do
|
|
root_account = account_model
|
|
Api.expects(:sis_parse_ids).with([1,2,3], "lookups", anything).returns({"users.id" => [4,5,6]})
|
|
Api.expects(:relation_for_sis_mapping_and_columns).with(User, {"users.id" => [4,5,6]}, {:lookups => "lookups"}, root_account).returns("params")
|
|
expect(Api.relation_for_sis_mapping(User, {:lookups => "lookups"}, [1,2,3], root_account)).to eq "params"
|
|
end
|
|
end
|
|
|
|
context 'relation_for_sis_mapping_and_columns' do
|
|
it 'should fail when not given a root account' do
|
|
expect(Api.relation_for_sis_mapping_and_columns(User, {}, {}, Account.default)).to eq User.none
|
|
expect(lambda {Api.relation_for_sis_mapping_and_columns(User, {}, {}, user)}).to raise_error("sis_root_account required for lookups")
|
|
end
|
|
|
|
it 'should properly generate an escaped arg string' do
|
|
expect(Api.relation_for_sis_mapping_and_columns(User, {"id" => ["1",2,3]}, {:scope => "scope"}, Account.default).to_sql).to match(/\(scope = #{Account.default.id} AND id IN \('1',2,3\)\)/)
|
|
end
|
|
|
|
it 'should work with no columns' do
|
|
expect(Api.relation_for_sis_mapping_and_columns(User, {}, {}, Account.default)).to eq User.none
|
|
end
|
|
|
|
it 'should add in joins if the sis_mapping has some with columns' do
|
|
expect(Api.relation_for_sis_mapping_and_columns(User, {"id" => ["1",2,3]}, {:scope => "scope", :joins => 'some joins'}, Account.default).eager_load_values).to eq ['some joins']
|
|
end
|
|
|
|
it 'should work with a few different column types and account scopings' do
|
|
expect(Api.relation_for_sis_mapping_and_columns(User, {"id1" => [1,2,3], "id2" => ["a","b","c"], "id3" => ["s1", "s2", "s3"]}, {:scope => "some_scope", :is_not_scoped_to_account => ['id3'].to_set}, Account.default).to_sql).to match(/\(\(some_scope = #{Account.default.id} AND id1 IN \(1,2,3\)\) OR \(some_scope = #{Account.default.id} AND id2 IN \('a','b','c'\)\) OR id3 IN \('s1','s2','s3'\)\)/)
|
|
end
|
|
|
|
it "should scope to accounts by default if :is_not_scoped_to_account doesn't exist" do
|
|
expect(Api.relation_for_sis_mapping_and_columns(User, {"id" => ["1",2,3]}, {:scope => "scope"}, Account.default).to_sql).to match(/\(scope = #{Account.default.id} AND id IN \('1',2,3\)\)/)
|
|
end
|
|
|
|
it "should fail if we're scoping to an account and the scope isn't provided" do
|
|
expect(lambda {Api.relation_for_sis_mapping_and_columns(User, {"id" => ["1",2,3]}, {}, Account.default)}).to raise_error("missing scope for collection")
|
|
end
|
|
end
|
|
|
|
context 'sis_mappings' do
|
|
it 'should correctly capture course lookups' do
|
|
lookups = Api.sis_find_sis_mapping_for_collection(Course)[:lookups]
|
|
expect(Api.sis_parse_id("sis_course_id:1", lookups)).to eq ["sis_source_id", "1"]
|
|
expect(Api.sis_parse_id("sis_term_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_user_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_login_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_account_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_section_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("1", lookups)).to eq ["id", 1]
|
|
end
|
|
|
|
it 'should correctly capture enrollment_term lookups' do
|
|
lookups = Api.sis_find_sis_mapping_for_collection(EnrollmentTerm)[:lookups]
|
|
expect(Api.sis_parse_id("sis_term_id:1", lookups)).to eq ["sis_source_id", "1"]
|
|
expect(Api.sis_parse_id("sis_course_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_user_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_login_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_account_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_section_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("1", lookups)).to eq ["id", 1]
|
|
end
|
|
|
|
it 'should correctly capture user lookups' do
|
|
lookups = Api.sis_find_sis_mapping_for_collection(User)[:lookups]
|
|
expect(Api.sis_parse_id("sis_user_id:1", lookups)).to eq ["pseudonyms.sis_user_id", "1"]
|
|
expect(Api.sis_parse_id("sis_login_id:1", lookups)).to eq ["LOWER(pseudonyms.unique_id)", "LOWER('1')"]
|
|
expect(Api.sis_parse_id("sis_course_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_term_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_account_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_section_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("1", lookups)).to eq ["users.id", 1]
|
|
end
|
|
|
|
it 'should correctly capture account lookups' do
|
|
lookups = Api.sis_find_sis_mapping_for_collection(Account)[:lookups]
|
|
expect(Api.sis_parse_id("sis_account_id:1", lookups)).to eq ["sis_source_id", "1"]
|
|
expect(Api.sis_parse_id("sis_course_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_term_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_user_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_login_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_section_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("1", lookups)).to eq ["id", 1]
|
|
end
|
|
|
|
it 'should correctly capture course_section lookups' do
|
|
lookups = Api.sis_find_sis_mapping_for_collection(CourseSection)[:lookups]
|
|
expect(Api.sis_parse_id("sis_section_id:1", lookups)).to eq ["sis_source_id", "1"]
|
|
expect(Api.sis_parse_id("sis_course_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_term_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_user_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_login_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("sis_account_id:1", lookups)).to eq [nil, nil]
|
|
expect(Api.sis_parse_id("1", lookups)).to eq ["id", 1]
|
|
end
|
|
|
|
it 'should correctly query the course table' do
|
|
sis_mapping = Api.sis_find_sis_mapping_for_collection(Course)
|
|
expect(Api.relation_for_sis_mapping_and_columns(Course, {"sis_source_id" => ["1"], "id" => ["1"]}, sis_mapping, Account.default).to_sql).to match(/id IN \('1'\) OR \(root_account_id = #{Account.default.id} AND sis_source_id IN \('1'\)\)/)
|
|
end
|
|
|
|
it 'should correctly query the enrollment_term table' do
|
|
sis_mapping = Api.sis_find_sis_mapping_for_collection(EnrollmentTerm)
|
|
expect(Api.relation_for_sis_mapping_and_columns(EnrollmentTerm, {"sis_source_id" => ["1"], "id" => ["1"]}, sis_mapping, Account.default).to_sql).to match(/id IN \('1'\) OR \(root_account_id = #{Account.default.id} AND sis_source_id IN \('1'\)\)/)
|
|
end
|
|
|
|
it 'should correctly query the user table' do
|
|
sis_mapping = Api.sis_find_sis_mapping_for_collection(User)
|
|
expect(Api.relation_for_sis_mapping_and_columns(User, {"pseudonyms.sis_user_id" => ["1"], "pseudonyms.unique_id" => ["1"], "users.id" => ["1"]}, sis_mapping, Account.default).to_sql).to match(/\(pseudonyms.account_id = #{Account.default.id} AND pseudonyms.sis_user_id IN \('1'\)\) OR \(pseudonyms.account_id = #{Account.default.id} AND pseudonyms.unique_id IN \('1'\)\) OR users.id IN \('1'\)/)
|
|
end
|
|
|
|
it 'should correctly query the account table' do
|
|
sis_mapping = Api.sis_find_sis_mapping_for_collection(Account)
|
|
expect(Api.relation_for_sis_mapping_and_columns(Account, {"sis_source_id" => ["1"], "id" => ["1"]}, sis_mapping, Account.default).to_sql).to match(/id IN \('1'\) OR \(root_account_id = #{Account.default.id} AND sis_source_id IN \('1'\)\)/)
|
|
end
|
|
|
|
it 'should correctly query the course_section table' do
|
|
sis_mapping = Api.sis_find_sis_mapping_for_collection(CourseSection)
|
|
expect(Api.relation_for_sis_mapping_and_columns(CourseSection, {"sis_source_id" => ["1"], "id" => ["1"]}, sis_mapping, Account.default).to_sql).to match(/id IN \('1'\) OR \(root_account_id = #{Account.default.id} AND sis_source_id IN \('1'\)\)/)
|
|
end
|
|
|
|
end
|
|
|
|
context "map_non_sis_ids" do
|
|
it 'should return an array of numeric ids' do
|
|
expect(Api.map_non_sis_ids([1, 2, 3, 4])).to eq [1, 2, 3, 4]
|
|
end
|
|
|
|
it 'should convert string ids to numeric' do
|
|
expect(Api.map_non_sis_ids(%w{5 4 3 2})).to eq [5, 4, 3, 2]
|
|
end
|
|
|
|
it "should exclude things that don't look like ids" do
|
|
expect(Api.map_non_sis_ids(%w{1 2 lolrus 4chan 5 6!})).to eq [1, 2, 5]
|
|
end
|
|
|
|
it "should strip whitespace" do
|
|
expect(Api.map_non_sis_ids([" 1", "2 ", " 3 ", "4\n"])).to eq [1, 2, 3, 4]
|
|
end
|
|
end
|
|
|
|
context 'ISO8601 regex' do
|
|
it 'should not allow invalid dates' do
|
|
expect('10/01/2014').to_not match Api::ISO8601_REGEX
|
|
end
|
|
|
|
it 'should not allow non ISO8601 dates' do
|
|
expect('2014-10-01').to_not match Api::ISO8601_REGEX
|
|
end
|
|
|
|
it 'should not allow garbage dates' do
|
|
expect('bad_data').to_not match Api::ISO8601_REGEX
|
|
end
|
|
|
|
it 'should allow valid dates' do
|
|
expect('2014-10-01T00:00:00-06:00').to match Api::ISO8601_REGEX
|
|
end
|
|
|
|
it 'should not allow valid dates BC' do
|
|
expect('-2014-10-01T00:00:00-06:00').to_not match Api::ISO8601_REGEX
|
|
end
|
|
end
|
|
|
|
context ".api_user_content" do
|
|
let(:klass) do
|
|
Class.new do
|
|
include Api
|
|
end
|
|
end
|
|
|
|
it "should ignore non-kaltura instructure_inline_media_comment links" do
|
|
student_in_course
|
|
html = %{<div>This is an awesome youtube:
|
|
<a href="http://www.example.com/" class="instructure_inline_media_comment">here</a>
|
|
</div>}
|
|
res = klass.new.api_user_content(html, @course, @student)
|
|
expect(res).to eq html
|
|
end
|
|
|
|
it 'prepends mobile css' do
|
|
student_in_course
|
|
account = @course.root_account
|
|
account.enable_feature!(:use_new_styles)
|
|
bc = BrandConfig.create(mobile_css_overrides: 'somewhere.css')
|
|
account.brand_config_md5 = bc.md5
|
|
account.save!
|
|
|
|
html = "<p>a</p><p>b</p>"
|
|
|
|
k = klass.new
|
|
k.stubs(:mobile_device?).returns(true)
|
|
res = k.api_user_content(html, @course, @student)
|
|
expect(res).to eq <<-HTML.strip
|
|
<link rel="stylesheet" href="somewhere.css"><p>a</p><p>b</p>
|
|
HTML
|
|
end
|
|
end
|
|
|
|
context ".process_incoming_html_content" do
|
|
class T
|
|
extend Api
|
|
end
|
|
|
|
it "should add context to files and remove verifier parameters" do
|
|
course
|
|
attachment_model(:context => @course)
|
|
|
|
html = %{<div>
|
|
Here are some bad links
|
|
<a href="/files/#{@attachment.id}/download">here</a>
|
|
<a href="/files/#{@attachment.id}/download?verifier=lollercopter&anotherparam=something">here</a>
|
|
<a href="/files/#{@attachment.id}/preview?sneakyparam=haha&verifier=lollercopter&another=blah">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/preview?noverifier=here">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/download?verifier=lol&a=1">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/download?b=2&verifier=something&c=2">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/notdownload?b=2&verifier=shouldstay&c=2">but not here</a>
|
|
</div>}
|
|
fixed_html = T.process_incoming_html_content(html)
|
|
expect(fixed_html).to eq %{<div>
|
|
Here are some bad links
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/download">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/download?anotherparam=something">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/preview?sneakyparam=haha&another=blah">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/preview?noverifier=here">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/download?a=1">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/download?b=2&c=2">here</a>
|
|
<a href="/courses/#{@course.id}/files/#{@attachment.id}/notdownload?b=2&verifier=shouldstay&c=2">but not here</a>
|
|
</div>}
|
|
end
|
|
end
|
|
|
|
context ".paginate" do
|
|
let(:request) { stub('request', query_parameters: {}) }
|
|
let(:response) { stub('response', headers: {}) }
|
|
let(:controller) { stub('controller', request: request, response: response, params: {}) }
|
|
|
|
describe "ordinal collection" do
|
|
let(:collection) { [1, 2, 3] }
|
|
|
|
it "should not raise Folio::InvalidPage for pages past the end" do
|
|
expect(Api.paginate(collection, controller, 'example.com', page: collection.size + 1, per_page: 1)).
|
|
to eq []
|
|
end
|
|
|
|
it "should not raise Folio::InvalidPage for integer-equivalent non-Integer pages" do
|
|
expect(Api.paginate(collection, controller, 'example.com', page: '1')).
|
|
to eq collection
|
|
end
|
|
|
|
it "should raise Folio::InvalidPage for pages <= 0" do
|
|
expect{ Api.paginate(collection, controller, 'example.com', page: 0) }.
|
|
to raise_error(Folio::InvalidPage)
|
|
|
|
expect{ Api.paginate(collection, controller, 'example.com', page: -1) }.
|
|
to raise_error(Folio::InvalidPage)
|
|
end
|
|
|
|
it "should raise Folio::InvalidPage for non-integer pages" do
|
|
expect{ Api.paginate(collection, controller, 'example.com', page: 'abc') }.
|
|
to raise_error(Folio::InvalidPage)
|
|
end
|
|
end
|
|
end
|
|
|
|
context ".build_links" do
|
|
it "should not build links if not pagination is provided" do
|
|
expect(Api.build_links("www.example.com")).to be_empty
|
|
end
|
|
|
|
it "should not build links for empty pages" do
|
|
expect(Api.build_links("www.example.com/", {
|
|
:per_page => 10,
|
|
:current => "",
|
|
:next => "",
|
|
:prev => "",
|
|
:first => "",
|
|
:last => "",
|
|
})).to be_empty
|
|
end
|
|
|
|
it "should build current, next, prev, first, and last links if provided" do
|
|
links = Api.build_links("www.example.com/", {
|
|
:per_page => 10,
|
|
:current => 8,
|
|
:next => 4,
|
|
:prev => 2,
|
|
:first => 1,
|
|
:last => 10,
|
|
})
|
|
expect(links.all?{ |l| l =~ /www.example.com\/\?/ }).to be_truthy
|
|
expect(links.find{ |l| l.match(/rel="current"/)}).to match /page=8&per_page=10>/
|
|
expect(links.find{ |l| l.match(/rel="next"/)}).to match /page=4&per_page=10>/
|
|
expect(links.find{ |l| l.match(/rel="prev"/)}).to match /page=2&per_page=10>/
|
|
expect(links.find{ |l| l.match(/rel="first"/)}).to match /page=1&per_page=10>/
|
|
expect(links.find{ |l| l.match(/rel="last"/)}).to match /page=10&per_page=10>/
|
|
end
|
|
|
|
it "should maintain query parameters" do
|
|
links = Api.build_links("www.example.com/", {
|
|
:query_parameters => { :search => "hihi" },
|
|
:per_page => 10,
|
|
:next => 2,
|
|
})
|
|
expect(links.first).to eq "<www.example.com/?search=hihi&page=2&per_page=10>; rel=\"next\""
|
|
end
|
|
|
|
it "should maintain array query parameters" do
|
|
links = Api.build_links("www.example.com/", {
|
|
:query_parameters => { :include => ["enrollments"] },
|
|
:per_page => 10,
|
|
:next => 2,
|
|
})
|
|
qs = "#{CGI.escape("include[]")}=enrollments"
|
|
expect(links.first).to eq "<www.example.com/?#{qs}&page=2&per_page=10>; rel=\"next\""
|
|
end
|
|
|
|
it "should not include certain sensitive params in the link headers" do
|
|
links = Api.build_links("www.example.com/", {
|
|
:query_parameters => { :access_token => "blah", :api_key => "xxx", :page => 3, :per_page => 10 },
|
|
:per_page => 10,
|
|
:next => 4,
|
|
})
|
|
expect(links.first).to eq "<www.example.com/?page=4&per_page=10>; rel=\"next\""
|
|
end
|
|
end
|
|
|
|
describe "#accepts_jsonapi?" do
|
|
class TestApiController
|
|
include Api
|
|
end
|
|
|
|
it "returns true when application/vnd.api+json in the Accept header" do
|
|
controller = TestApiController.new
|
|
controller.stubs(:request).returns stub(headers: {
|
|
'Accept' => 'application/vnd.api+json'
|
|
})
|
|
expect(controller.accepts_jsonapi?).to eq true
|
|
end
|
|
|
|
it "returns false when application/vnd.api+json not in the Accept header" do
|
|
controller = TestApiController.new
|
|
controller.stubs(:request).returns stub(headers: {
|
|
'Accept' => 'application/json'
|
|
})
|
|
expect(controller.accepts_jsonapi?).to eq false
|
|
end
|
|
end
|
|
|
|
describe ".value_to_array" do
|
|
it "splits comma delimited strings" do
|
|
expect(Api.value_to_array('1,2,3')).to eq ['1', '2', '3']
|
|
end
|
|
|
|
it "does nothing to arrays" do
|
|
expect(Api.value_to_array(['1', '2', '3'])).to eq ['1', '2', '3']
|
|
end
|
|
|
|
it "returns an empty array for nil" do
|
|
expect(Api.value_to_array(nil)).to eq []
|
|
end
|
|
end
|
|
|
|
describe "#templated_url" do
|
|
before do
|
|
@api = TestApiInstance.new Account.default, nil
|
|
end
|
|
|
|
it "should return url with a single item" do
|
|
url = @api.templated_url(:account_url, "{courses.account}")
|
|
expect(url).to eq "http://www.example.com/accounts/{courses.account}"
|
|
end
|
|
|
|
it "should return url with multiple items" do
|
|
url = @api.templated_url(:course_assignment_url, "{courses.id}", "{courses.assignment}")
|
|
expect(url).to eq "http://www.example.com/courses/{courses.id}/assignments/{courses.assignment}"
|
|
end
|
|
|
|
it "should return url with no template items" do
|
|
url = @api.templated_url(:account_url, "1}")
|
|
expect(url).to eq "http://www.example.com/accounts/1%7D"
|
|
end
|
|
|
|
it "should return url with a combination of items" do
|
|
url = @api.templated_url(:course_assignment_url, "{courses.id}", "1}")
|
|
expect(url).to eq "http://www.example.com/courses/{courses.id}/assignments/1%7D"
|
|
end
|
|
end
|
|
end
|