canvas-lms/spec/models/active_record_base_spec.rb

602 lines
19 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__) + '/../spec_helper.rb')
describe ActiveRecord::Base do
describe "count_by_date" do
let_once(:account) { Account.create! }
def create_courses(account, start_times)
start_times.each_with_index do |time, i|
(i + 1).times do
course = account.courses.build
course.start_at = time
course.save!
end
end
end
it "should work" do
start_times = [
Time.zone.now,
Time.zone.now.advance(:days => -1),
Time.zone.now.advance(:days => -2),
Time.zone.now.advance(:days => -3)
]
create_courses(account, start_times)
# updated_at
account.courses.count_by_date.should eql({start_times.first.to_date => 10})
account.courses.count_by_date(:column => :start_at).should eql Hash[
start_times.each_with_index.map{ |t, i| [t.to_date, i + 1]}
]
end
it "should just do the last 20 days by default" do
start_times = [
Time.zone.now,
Time.zone.now.advance(:days => -19),
Time.zone.now.advance(:days => -20),
Time.zone.now.advance(:days => 1)
]
create_courses(account, start_times)
# updated_at
account.courses.count_by_date.should eql({start_times.first.to_date => 10})
account.courses.count_by_date(:column => :start_at).should eql Hash[
start_times[0..1].each_with_index.map{ |t, i| [t.to_date, i + 1]}
]
end
end
describe "find in batches" do
before :once do
@c1 = course(:name => 'course1', :active_course => true)
@c2 = course(:name => 'course2', :active_course => true)
u1 = user(:name => 'user1', :active_user => true)
u2 = user(:name => 'user2', :active_user => true)
u3 = user(:name => 'user3', :active_user => true)
@e1 = @c1.enroll_student(u1, :enrollment_state => 'active')
@e2 = @c1.enroll_student(u2, :enrollment_state => 'active')
@e3 = @c1.enroll_student(u3, :enrollment_state => 'active')
@e4 = @c2.enroll_student(u1, :enrollment_state => 'active')
@e5 = @c2.enroll_student(u2, :enrollment_state => 'active')
@e6 = @c2.enroll_student(u3, :enrollment_state => 'active')
end
it "should find all enrollments from course join in batches" do
e = Course.active.where(id: [@c1, @c2]).select("enrollments.id AS e_id").
joins(:enrollments).order("e_id asc")
batch_size = 2
es = []
e.find_in_batches_with_temp_table(:batch_size => batch_size) do |batch|
batch.size.should == batch_size
batch.each do |r|
es << r["e_id"].to_i
end
end
es.length.should == 6
es.should == [@e1.id,@e2.id,@e3.id,@e4.id,@e5.id,@e6.id]
end
it "should honor includes when using a cursor" do
pending "needs PostgreSQL" unless Account.connection.adapter_name == 'PostgreSQL'
Account.default.courses.create!
Account.transaction do
Account.where(:id => Account.default).includes(:courses).find_each do |a|
a.courses.loaded?.should be_true
end
end
end
it "should not use a cursor when start is passed" do
pending "needs PostgreSQL" unless Account.connection.adapter_name == 'PostgreSQL'
Account.transaction do
Account.expects(:find_in_batches_with_cursor).never
Account.where(:id => Account.default).includes(:courses).find_each(start: 0) do |a|
a.courses.loaded?.should be_true
end
end
end
it "should raise an error when start is used with group" do
lambda { Account.group(:id).find_each(start: 0) }.should raise_error(ArgumentError)
end
end
describe "#remove_dropped_columns" do
before do
@orig_dropped = ActiveRecord::Base::DROPPED_COLUMNS
end
after do
ActiveRecord::Base.send(:remove_const, :DROPPED_COLUMNS)
ActiveRecord::Base::DROPPED_COLUMNS = @orig_dropped
User.reset_column_information
end
it "should mask columns marked as dropped from column info methods" do
User.columns.any? { |c| c.name == 'name' }.should be_true
User.column_names.should be_include('name')
u = User.create!(:name => 'my name')
# if we ever actually drop the name column, this spec will fail on the line
# above, so it's all good
ActiveRecord::Base.send(:remove_const, :DROPPED_COLUMNS)
ActiveRecord::Base::DROPPED_COLUMNS = { 'users' => %w(name) }
User.reset_column_information
User.columns.any? { |c| c.name == 'name' }.should be_false
User.column_names.should_not be_include('name')
# load from the db should hide the attribute
u = User.find(u.id)
u.attributes.keys.include?('name').should be_false
end
it "should only drop columns from the specific table specified" do
ActiveRecord::Base.send(:remove_const, :DROPPED_COLUMNS)
ActiveRecord::Base::DROPPED_COLUMNS = { 'users' => %w(name) }
User.reset_column_information
Group.reset_column_information
User.columns.any? { |c| c.name == 'name' }.should be_false
Group.columns.any? { |c| c.name == 'name' }.should be_true
end
end
context "rank helpers" do
it "should generate appropriate rank sql" do
ActiveRecord::Base.rank_sql(['a', ['b', 'c'], ['d']], 'foo').
should eql "CASE WHEN foo IN ('a') THEN 0 WHEN foo IN ('b', 'c') THEN 1 WHEN foo IN ('d') THEN 2 ELSE 3 END"
end
it "should generate appropriate rank hashes" do
hash = ActiveRecord::Base.rank_hash(['a', ['b', 'c'], ['d']])
hash.should == {'a' => 1, 'b' => 2, 'c' => 2, 'd' => 3}
hash['e'].should eql 4
end
end
it "should have a valid GROUP BY clause when group_by is used correctly" do
conn = ActiveRecord::Base.connection
lambda {
User.find_by_sql "SELECT id, name FROM users GROUP BY #{conn.group_by('id', 'name')}"
User.find_by_sql "SELECT id, name FROM (SELECT id, name FROM users) u GROUP BY #{conn.group_by('id', 'name')}"
}.should_not raise_error
end
context "unique_constraint_retry" do
before :once do
@user = user_model
@assignment = assignment_model
@orig_user_count = User.count
end
it "should normally run once" do
User.unique_constraint_retry do
User.create!
end
User.count.should eql @orig_user_count + 1
end
it "should run twice if it gets a UniqueConstraintViolation" do
Submission.create!(:user => @user, :assignment => @assignment)
tries = 0
lambda{
User.unique_constraint_retry do
tries += 1
User.create!
Submission.create!(:user => @user, :assignment => @assignment)
end
}.should raise_error(ActiveRecord::Base::UniqueConstraintViolation) # we don't catch the error the second time
Submission.count.should eql 1
tries.should eql 2
User.count.should eql @orig_user_count
end
it "should run additional times if specified" do
Submission.create!(:user => @user, :assignment => @assignment)
tries = 0
lambda{
User.unique_constraint_retry(2) do
tries += 1
Submission.create!(:user => @user, :assignment => @assignment)
end
}.should raise_error # we don't catch the error the last time
tries.should eql 3
Submission.count.should eql 1
end
it "should not cause outer transactions to roll back if the second attempt succeeds" do
Submission.create!(:user => @user, :assignment => @assignment)
tries = 0
User.transaction do
User.create!
User.unique_constraint_retry do
tries += 1
User.create!
Submission.create!(:user => @user, :assignment => @assignment) if tries == 1
end
User.create!
end
Submission.count.should eql 1
User.count.should eql @orig_user_count + 3
end
it "should not eat other ActiveRecord::StatementInvalid exceptions" do
tries = 0
lambda {
User.unique_constraint_retry {
tries += 1
User.connection.execute "this is not valid sql"
}
}.should raise_error(ActiveRecord::StatementInvalid)
tries.should eql 1
end
it "should not eat any other exceptions" do
tries = 0
lambda {
User.unique_constraint_retry {
tries += 1
raise "oh crap"
}
}.should raise_error
tries.should eql 1
end
end
# see config/initializers/rails_patches.rb
context "query cache" do
it "should clear the query cache on a successful insert" do
User.create
User.cache do
User.first
User.connection.expects(:select).never
User.first
User.connection.unstub(:select)
User.create!
User.connection.expects(:select).once.returns([])
User.first
end
end
it "should clear the query cache on an unsuccessful insert" do
u = User.create
User.cache do
User.first
User.connection.expects(:select).never
User.first
User.connection.unstub(:select)
u2 = User.new
u2.id = u.id
lambda{ u2.save! }.should raise_error(ActiveRecord::Base::UniqueConstraintViolation)
User.connection.expects(:select).once.returns([])
User.first
end
end
end
context "add_polymorphs" do
class OtherPolymorphyThing; end
before :all do
# it already has :submission
ConversationMessage.add_polymorph_methods :asset, [:other_polymorphy_thing]
end
before :once do
@conversation = Conversation.create
@user = user_model
@assignment = assignment_model
end
context "getter" do
it "should return the polymorph" do
sub = @user.submissions.create!(:assignment => @assignment)
m = @conversation.conversation_messages.build
m.asset = sub
m.submission.should be_an_instance_of(Submission)
end
it "should not return the polymorph if the type is wrong" do
m = @conversation.conversation_messages.build
m.asset = @user.submissions.create!(:assignment => @assignment)
m.other_polymorphy_thing.should be_nil
end
end
context "setter" do
it "should set the underlying association" do
m = @conversation.conversation_messages.build
s = @user.submissions.create!(:assignment => @assignment)
m.submission = s
m.asset_type.should eql 'Submission'
m.asset_id.should eql s.id
m.asset.should eql s
m.submission.should eql s
m.submission = nil
m.asset_type.should be_nil
m.asset_id.should be_nil
m.asset.should be_nil
m.submission.should be_nil
end
it "should not change the underlying association if it's another object and we're setting nil" do
m = @conversation.conversation_messages.build
s = @user.submissions.create!(:assignment => @assignment)
m.submission = s
m.other_polymorphy_thing = nil
m.asset_type.should eql 'Submission'
m.asset_id.should eql s.id
m.asset.should eql s
m.submission.should eql s
m.other_polymorphy_thing.should be_nil
end
end
end
context "bulk_insert" do
it "should work" do
User.bulk_insert [
{:name => "bulk_insert_1", :workflow_state => "registered"},
{:name => "bulk_insert_2", :workflow_state => "registered"}
]
names = User.order(:name).pluck(:name)
names.should be_include("bulk_insert_1")
names.should be_include("bulk_insert_2")
end
it "should not raise an error if there are no records" do
expect { Course.bulk_insert [] }.to change(Course, :count).by(0)
end
end
context "distinct" do
before :once do
User.create()
User.create()
User.create(:locale => "en")
User.create(:locale => "en")
User.create(:locale => "es")
end
it "should return distinct values" do
User.distinct(:locale).should eql ["en", "es"]
end
it "should return distinct values with nil" do
User.distinct(:locale, :include_nil => true).should eql [nil, "en", "es"]
end
end
context "find_ids_in_batches" do
it "should return ids from the table in batches of specified size" do
ids = []
5.times { ids << User.create!().id }
batches = []
User.where(id: ids).find_ids_in_batches(:batch_size => 2) do |found_ids|
batches << found_ids
end
batches.should == [ ids[0,2], ids[2,2], ids[4,1] ]
end
end
describe "find_ids_in_ranges" do
it "should return ids from the table in ranges" do
ids = []
10.times { ids << User.create!().id }
batches = []
User.where(id: ids).find_ids_in_ranges(:batch_size => 4) do |*found_ids|
batches << found_ids
end
batches.should == [ [ids[0], ids[3]],
[ids[4], ids[7]],
[ids[8], ids[9]] ]
end
it "should work with scopes" do
user = User.create!
user2 = User.create!
user2.destroy
User.active.where(id: [user, user2]).find_ids_in_ranges do |*found_ids|
found_ids.should == [user.id, user.id]
end
end
end
context "after_transaction_commit" do
self.use_transactional_fixtures = false
before do
Rails.env.stubs(:test?).returns(false)
end
it "should execute the callback immediately if not in a transaction" do
a = 0
User.connection.after_transaction_commit { a += 1 }
a.should == 1
end
it "should execute the callback after commit if in a transaction" do
a = 0
User.connection.transaction do
User.connection.after_transaction_commit { a += 1 }
a.should == 0
end
a.should == 1
end
it "should not execute the callbacks on rollback" do
a = 0
User.connection.transaction do
User.connection.after_transaction_commit { a += 1 }
a.should == 0
raise ActiveRecord::Rollback
end
a.should == 0
User.connection.transaction do
# verify that the callback gets cleared out, so this second transaction won't trigger it
end
a.should == 0
end
it "should avoid loops due to callbacks causing a new transaction" do
a = 0
User.connection.transaction do
User.connection.after_transaction_commit { User.connection.transaction { a += 1 } }
a.should == 0
end
a.should == 1
end
end
context "Finder tests" do
before :once do
@user = user_model
end
it "should fail with improper nested hashes" do
expect {
User.where(:name => { :users => { :id => @user }}).first
}.to raise_error(ActiveRecord::StatementInvalid)
end
it "should fail with dot in nested column name" do
expect {
User.where(:name => { "users.id" => @user }).first
}.to raise_error(ActiveRecord::StatementInvalid)
end
it "should not fail with a dot in column name only" do
User.where('users.id' => @user).first.should_not be_nil
end
end
describe "find_by_asset_string" do
it "should enforce type restrictions" do
u = User.create!
ActiveRecord::Base.find_by_asset_string(u.asset_string).should == u
ActiveRecord::Base.find_by_asset_string(u.asset_string, ['User']).should == u
ActiveRecord::Base.find_by_asset_string(u.asset_string, ['Course']).should == nil
end
end
describe "update_all/delete_all with_joins" do
before :once do
@u1 = User.create!(:name => 'a')
@u2 = User.create!(:name => 'b')
@p1 = @u1.pseudonyms.create!(:unique_id => 'pa', :account => Account.default)
@p1_2 = @u1.pseudonyms.create!(:unique_id => 'pa2', :account => Account.default)
@p2 = @u2.pseudonyms.create!(:unique_id => 'pb', :account => Account.default)
@p1_2.destroy
end
before do
pending "MySQL and Postgres only" unless %w{PostgreSQL MySQL Mysql2}.include?(ActiveRecord::Base.connection.adapter_name)
end
it "should do an update all with a join" do
Pseudonym.joins(:user).active.where(:users => {:name => 'a'}).update_all(:unique_id => 'pa3')
@p1.reload.unique_id.should == 'pa3'
@p1_2.reload.unique_id.should == 'pa2'
@p2.reload.unique_id.should == 'pb'
end
it "should do a delete all with a join" do
Pseudonym.joins(:user).active.where(:users => {:name => 'a'}).delete_all
lambda { @p1.reload }.should raise_error(ActiveRecord::RecordNotFound)
@u1.reload.should_not be_deleted
@p1_2.reload.unique_id.should == 'pa2'
@p2.reload.unique_id.should == 'pb'
end
end
describe "delete_all with_limit" do
it "should work" do
u = User.create!
p1 = u.pseudonyms.create!(unique_id: 'a', account: Account.default)
p2 = u.pseudonyms.create!(unique_id: 'b', account: Account.default)
u.pseudonyms.scoped.reorder("unique_id DESC").limit(1).delete_all
p1.reload
lambda { p2.reload }.should raise_error(ActiveRecord::RecordNotFound)
end
end
describe "add_index" do
it "should raise an error on too long of name" do
name = 'some_really_long_name_' * 10
lambda { User.connection.add_index :users, [:id], name: name }.should raise_error
end
end
describe "nested conditions" do
it "should not barf if the condition has a question mark" do
User.joins(:enrollments).where(enrollments: { sis_source_id: 'a?c'}).first.should be_nil
end
end
describe ".nulls" do
before :once do
@u1 = User.create!
User.where(id: @u1).update_all(name: nil)
@u2 = User.create!(name: 'a')
@u3 = User.create!
User.where(id: @u3).update_all(name: nil)
@u4 = User.create!(name: 'b')
@us = [@u1, @u2, @u3, @u4]
# for sanity
User.where(id: @us, name: nil).order(:id).all.should == [@u1, @u3]
end
it "should sort nulls first" do
User.where(id: @us).order(User.nulls(:first, :name), :id).all.should == [@u1, @u3, @u2, @u4]
end
it "should sort nulls last" do
User.where(id: @us).order(User.nulls(:last, :name), :id).all.should == [@u2, @u4, @u1, @u3]
end
it "should sort nulls first, desc" do
User.where(id: @us).order(User.nulls(:first, :name, :desc), :id).all.should == [@u1, @u3, @u4, @u2]
end
it "should sort nulls last, desc" do
User.where(id: @us).order(User.nulls(:last, :name, :desc), :id).all.should == [@u4, @u2, @u1, @u3]
end
end
describe "marshalling" do
it "should not load associations when marshalling" do
a = Account.default
a.user_account_associations.loaded?.should be_false
Marshal.dump(a)
a.user_account_associations.loaded?.should be_false
end
end
end