canvas-lms/spec/lib/user_merge_spec.rb

1132 lines
53 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2013 - present 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/>.
describe UserMerge do
describe 'with simple users' do
let!(:user1) { user_model }
let!(:user2) { user_model }
let(:course1) { course_factory(active_all: true) }
let(:course2) { course_factory(active_all: true) }
it 'deletes the old user' do
UserMerge.from(user2).into(user1)
user1.reload
user2.reload
expect(user1).not_to be_deleted
expect(user2).to be_deleted
end
it 'fails if a user is a test user' do
fake_student = course1.student_view_student
expect { UserMerge.from(user2).into(fake_student) }.to raise_error('cannot merge a test student')
end
it 'logs who did the user merge' do
merger = user_model
mergeme = UserMerge.from(user2)
mergeme.into(user1, merger: merger, source: 'this spec')
expect(mergeme.merge_data.items.where(item_type: 'logs').take.item).to eq "{:merger_id=>#{merger.id}, :source=>\"this spec\"}"
end
it 'marks as failed on merge failures' do
mergeme = UserMerge.from(user2)
# make any method that gets called raise an error
allow(mergeme).to receive(:copy_favorites).and_raise('boom')
expect { mergeme.into(user1) }.to raise_error('boom')
expect(mergeme.merge_data.workflow_state).to eq 'failed'
expect(mergeme.merge_data.items.where(item_type: 'merge_error').take.item.first).to eq 'boom'
end
it 'records where the user was merged to' do
UserMerge.from(user2).into(user1)
expect(user2.reload.merged_into_user).to eq user1
end
it "moves pseudonyms to the new user" do
pseudonym = user2.pseudonyms.create!(unique_id: 'sam@yahoo.com')
pseudonym2 = user2.pseudonyms.create!(unique_id: 'sam2@yahoo.com')
UserMerge.from(user2).into(user1)
merge_data = UserMergeData.where(user_id: user1).first
expect(merge_data.from_user_id).to eq user2.id
expect(merge_data.records.pluck(:context_id).sort).to eq [pseudonym.id, pseudonym2.id].sort
user2.reload
expect(user2.pseudonyms).to be_empty
user1.reload
expect(user1.pseudonyms.map(&:unique_id)).to be_include('sam@yahoo.com')
end
it "moves lti_id to the new users" do
user_1_old_lti = user1.lti_id
old_lti = user2.lti_id
old_lti_context = user2.lti_context_id
course1.enroll_user(user1)
course2.enroll_user(user2)
UserMerge.from(user2).into(user1)
expect(user1.reload.past_lti_ids.take.user_lti_id).to eq old_lti
expect(user1.past_lti_ids.take.user_lti_context_id).to eq old_lti_context
user3 = user_model
UserMerge.from(user1).into(user3)
expect(user3.reload.past_lti_ids.where(context_id: course1).take.user_lti_id).to eq user_1_old_lti
expect(user3.past_lti_ids.where(context_id: course2).take.user_lti_id).to eq old_lti
end
it "moves past_lti_id to the new user multiple merges with conflict" do
course1.enroll_user(user1)
course2.enroll_user(user2)
UserPastLtiId.create!(user: user2, context: course2, user_uuid: 'fake_uuid', user_lti_id: 'fake_lti_id_from_old_merge')
UserMerge.from(user2).into(user1)
expect(user1.reload.past_lti_ids.take.user_lti_id).to eq 'fake_lti_id_from_old_merge'
end
it "moves admins to the new user" do
account1 = account_model
admin = account1.account_users.create(user: user2)
UserMerge.from(user2).into(user1)
merge_data = UserMergeData.where(user_id: user1).first
expect(merge_data.from_user_id).to eq user2.id
expect(merge_data.records.where(context_type: 'AccountUser').first.context_id).to eq admin.id
user1.reload
expect(user1.account_users.first.id).to eq admin.id
end
it "uses avatar information from merged user if none exists" do
user2.avatar_image = { 'type' => 'external', 'url' => 'https://example.com/image.png' }
user2.save!
UserMerge.from(user2).into(user1)
user1.reload
user2.reload
[:avatar_image_source, :avatar_image_url, :avatar_image_updated_at, :avatar_state].each do |attr|
expect(user1[attr]).to eq user2[attr]
end
end
it "does not overwrite avatar information already in place" do
user1.avatar_state = 'locked'
user1.save!
user2.avatar_image = { 'type' => 'external', 'url' => 'https://example.com/image.png' }
user2.save!
UserMerge.from(user2).into(user1)
user1.reload
user2.reload
expect(user1.avatar_state).not_to eq user2.avatar_state
end
it "moves access tokens to the new user" do
at = AccessToken.create!(:user => user2, :developer_key => DeveloperKey.default)
UserMerge.from(user2).into(user1)
at.reload
expect(at.user_id).to eq user1.id
end
it "moves submissions to the new user (but only if they don't already exist)" do
a1 = assignment_model
s1 = a1.find_or_create_submission(user1)
s1.submission_type = "online_quiz"
s1.save!
s2 = a1.find_or_create_submission(user2)
s2.submission_type = "online_quiz"
s2.save!
a2 = assignment_model
s3 = a2.find_or_create_submission(user2)
s3.submission_type = "online_quiz"
s3.save!
expect(user2.submissions.length).to eql(2)
expect(user1.submissions.length).to eql(1)
UserMerge.from(user2).into(user1)
user2.reload
user1.reload
expect(user2.submissions.length).to eql(1)
expect(user2.submissions.first.id).to eql(s2.id)
expect(user1.submissions.length).to eql(2)
expect(user1.submissions.map(&:id)).to be_include(s1.id)
expect(user1.submissions.map(&:id)).to be_include(s3.id)
end
it "does not move or delete submission when both users have submissions" do
a1 = assignment_model
s1 = a1.find_or_create_submission(user1)
s1.submission_type = "online_quiz"
s1.save!
s2 = a1.find_or_create_submission(user2)
s2.submission_type = "online_quiz"
s2.save!
UserMerge.from(user1).into(user2)
expect(user1.reload.submissions).to eq [s1.reload]
expect(user2.reload.submissions).to eq [s2.reload]
end
it "prioritizes grades over submissions" do
a1 = assignment_model(course: course1)
course1.enroll_user(user1)
s1 = a1.grade_student(user1, grade: "10", grader: @teacher).first
s2 = a1.find_or_create_submission(user2)
s2.submission_type = "online_quiz"
s2.save!
UserMerge.from(user1).into(user2)
expect(user1.reload.submissions).to eq [s2.reload]
expect(user2.reload.submissions).to eq [s1.reload]
end
it "moves and swap submission when one user has a submission" do
a2 = assignment_model
s3 = a2.find_or_create_submission(user1)
s3.submission_type = "online_quiz"
s3.save!
s4 = a2.find_or_create_submission(user2)
UserMerge.from(user1).into(user2)
expect(user1.reload.submissions).to eq [s4.reload]
expect(user2.reload.submissions).to eq [s3.reload]
end
it "moves quiz submissions to the new user (but only if they don't already exist)" do
q1 = quiz_model
qs1 = q1.generate_submission(user1)
qs2 = q1.generate_submission(user2)
sub = submission_model(user: user2)
sub.quiz_submission_id = qs2
sub.save!
qs2.submission_id = sub
qs2.save!
q2 = quiz_model
qs3 = q2.generate_submission(user2)
expect(user1.quiz_submissions.length).to be(1)
expect(user2.quiz_submissions.length).to be(2)
UserMerge.from(user2).into(user1)
user2.reload
user1.reload
expect(user2.quiz_submissions.length).to be(1)
expect(user2.quiz_submissions.first.id).to be(qs1.id)
expect(qs2.reload.submission_id).to eq sub.id
expect(user1.quiz_submissions.length).to be(2)
expect(user1.quiz_submissions.map(&:id)).to be_include(qs2.id)
expect(user1.quiz_submissions.map(&:id)).to be_include(qs3.id)
end
it "moves ccs to the new user (but only if they don't already exist)" do
# unconfirmed => active conflict
communication_channel(user1, { username: 'a@instructure.com' })
communication_channel(user2, { username: 'A@instructure.com', active_cc: true })
# active => unconfirmed conflict
cc1 = communication_channel(user1, { username: 'b@instructure.com', active_cc: true })
communication_channel(user2, { username: 'B@instructure.com' })
# active => active conflict
communication_channel(user1, { username: 'c@instructure.com', active_cc: true })
communication_channel(user2, { username: 'C@instructure.com', active_cc: true })
# unconfirmed => unconfirmed conflict
communication_channel(user1, { username: 'd@instructure.com' })
communication_channel(user2, { username: 'D@instructure.com' })
# retired => unconfirmed conflict
communication_channel(user1, { username: 'e@instructure.com', cc_state: 'retired' })
communication_channel(user2, { username: 'E@instructure.com' })
# unconfirmed => retired conflict
communication_channel(user1, { username: 'f@instructure.com' })
communication_channel(user2, { username: 'F@instructure.com', cc_state: 'retired' })
# retired => active conflict
communication_channel(user1, { username: 'g@instructure.com', cc_state: 'retired' })
communication_channel(user2, { username: 'G@instructure.com', cc_state: 'active' })
# active => retired conflict
communication_channel(user1, { username: 'h@instructure.com', cc_state: 'active' })
communication_channel(user2, { username: 'H@instructure.com', cc_state: 'retired' })
# retired => retired conflict
communication_channel(user1, { username: 'i@instructure.com', cc_state: 'retired' })
communication_channel(user2, { username: 'I@instructure.com', cc_state: 'retired' })
# <nothing> => active
communication_channel(user2, { username: 'j@instructure.com', active_cc: true })
# active => <nothing>
communication_channel(user1, { username: 'k@instructure.com', active_cc: true })
# <nothing> => unconfirmed
communication_channel(user2, { username: 'l@instructure.com' })
# unconfirmed => <nothing>
communication_channel(user1, { username: 'm@instructure.com' })
# <nothing> => retired
communication_channel(user2, { username: 'n@instructure.com', cc_state: 'retired' })
# retired => <nothing>
communication_channel(user1, { username: 'o@instructure.com', cc_state: 'retired' })
UserMerge.from(user1).into(user2)
user1.reload
user2.reload
records = UserMergeData.where(user_id: user2).take.records
expect(records.count).to eq 8
record = records.where(context_id: cc1).take
expect(record.previous_user_id).to eq user1.id
expect(record.previous_workflow_state).to eq 'active'
expect(record.context_type).to eq 'CommunicationChannel'
expect(user2.communication_channels.map { |cc| [cc.path, cc.workflow_state] }.sort).to match_array([
['A@instructure.com', 'active'],
['C@instructure.com', 'active'],
['D@instructure.com', 'unconfirmed'],
['E@instructure.com', 'unconfirmed'],
['G@instructure.com', 'active'],
['I@instructure.com', 'retired'],
['b@instructure.com', 'active'],
['f@instructure.com', 'unconfirmed'],
['h@instructure.com', 'active'],
['j@instructure.com', 'active'],
['k@instructure.com', 'active'],
['l@instructure.com', 'unconfirmed'],
['m@instructure.com', 'unconfirmed'],
['n@instructure.com', 'retired']
])
expect(user1.communication_channels.map { |cc| [cc.path, cc.workflow_state] }.sort).to match_array([
['a@instructure.com', 'retired'],
['c@instructure.com', 'retired'],
['d@instructure.com', 'retired'],
['e@instructure.com', 'retired'],
['g@instructure.com', 'retired'],
['i@instructure.com', 'retired'],
['o@instructure.com', 'retired']
])
%w{B@instructure.com F@instructure.com H@instructure.com}.each do |path|
expect(CommunicationChannel.where(user_id: [user1, user2]).by_path(path).detect { |cc| cc.path == path }).to be_nil
end
end
it "moves and uniquify enrollments" do
enrollment1 = course1.enroll_user(user1)
enrollment2 = course1.enroll_student(user2, enrollment_state: 'active')
section = course1.course_sections.create!
enrollment3 = course1.enroll_student(user1,
enrollment_state: 'invited',
allow_multiple_enrollments: true,
section: section)
enrollment4 = course1.enroll_teacher(user1)
UserMerge.from(user1).into(user2)
merge_data = UserMergeData.where(user_id: user2).first
expect(merge_data.records.pluck(:context_id).sort)
.to eq [enrollment1.id, enrollment3.id, enrollment4.id].sort
enrollment1.reload
expect(enrollment1.user).to eq user1
expect(enrollment1).to be_deleted
merge_data_record = merge_data.records.where(context_id: enrollment1).first
expect(merge_data_record.previous_workflow_state).to eq 'invited'
enrollment2.reload
expect(enrollment2).to be_active
expect(enrollment2.user).to eq user2
enrollment3.reload
expect(enrollment3).to be_invited
enrollment4.reload
expect(enrollment4.user).to eq user2
expect(enrollment4).to be_invited
user1.reload
expect(user1.enrollments).to eq [enrollment1]
end
it "handles enrollment conflicts like a champ" do
enrollment1 = course1.enroll_student(user1, enrollment_state: 'invited')
enrollment2 = course1.enroll_student(user2, enrollment_state: 'active')
UserMerge.from(user2).into(user1)
merge_data = UserMergeData.where(user_id: user1).first
expect(merge_data.records.pluck(:context_id).sort)
.to eq [enrollment1.id, enrollment2.id].sort
enrollment1.reload
expect(enrollment1.user).to eq user1
expect(enrollment1.workflow_state).to eq 'active'
expect(enrollment1.enrollment_state.state).to eq 'active'
merge_data_record = merge_data.records.where(context_id: enrollment1).first
expect(merge_data_record.previous_workflow_state).to eq 'invited'
enrollment2.reload
expect(enrollment2.user).to eq user2
expect(enrollment2.workflow_state).to eq 'deleted'
expect(enrollment2.enrollment_state.state).to eq 'deleted'
merge_data_record2 = merge_data.records.where(context_id: enrollment2).first
expect(merge_data_record2.previous_workflow_state).to eq 'active'
end
it "removes conflicting module progressions" do
course1.enroll_user(user1, 'StudentEnrollment', enrollment_state: 'active')
course1.enroll_user(user2, 'StudentEnrollment', enrollment_state: 'active')
assignment = course1.assignments.create!(title: "some assignment")
assignment2 = course1.assignments.create!(title: "some second assignment")
context_module = course1.context_modules.create!(name: "some module")
context_module2 = course1.context_modules.create!(name: "some second module")
tag = context_module.add_item(id: assignment, type: 'assignment')
tag2 = context_module2.add_item(id: assignment2, type: 'assignment')
context_module.completion_requirements = { tag.id => { type: 'must_view' } }
context_module2.completion_requirements = { tag2.id => { type: 'min_score', min_score: 5 } }
context_module.save
context_module2.save
# have a conflicting module_progrssion
assignment2.grade_student(user1, :grade => "10", grader: @teacher)
assignment2.grade_student(user2, :grade => "4", grader: @teacher)
# have a duplicate module_progression
context_module.update_for(user1, :read, tag)
context_module.update_for(user2, :read, tag)
# it should work
expect { UserMerge.from(user1).into(user2) }.to_not raise_error
# it should have deleted or moved the module progressions for User1 and kept the completed ones for user2
expect(ContextModuleProgression.where(user_id: user1, context_module_id: [context_module, context_module2]).count).to eq 0
expect(ContextModuleProgression.where(user_id: user2, context_module_id: [context_module, context_module2], workflow_state: 'completed').count).to eq 2
end
it "removes observer enrollments that observe themselves (target)" do
enrollment1 = course1.enroll_user(user1, 'StudentEnrollment', enrollment_state: 'active')
enrollment2 = course1.enroll_user(user2, 'ObserverEnrollment', enrollment_state: 'active', associated_user_id: user1.id)
UserMerge.from(user1).into(user2)
merge_data = UserMergeData.where(user_id: user2).first
o = merge_data.records.where(context_id: enrollment2).first
expect(o.previous_workflow_state).to eq 'active'
expect(enrollment1.reload.user).to eql user2
expect(enrollment2.reload.workflow_state).to eql 'deleted'
end
it "removes observer enrollments that observe themselves (source)" do
enrollment1 = course1.enroll_user(user1, 'StudentEnrollment', enrollment_state: 'active')
enrollment2 = course1.enroll_user(user2, 'ObserverEnrollment', enrollment_state: 'active', associated_user_id: user1.id)
UserMerge.from(user2).into(user1)
expect(enrollment1.reload.user).to eql user1
expect(enrollment2.reload.workflow_state).to eql 'deleted'
end
it "moves and uniquify observee enrollments" do
course2
course1.enroll_user(user1)
course1.enroll_user(user2)
observer1 = user_with_pseudonym
observer2 = user_with_pseudonym
add_linked_observer(user1, observer1)
add_linked_observer(user1, observer2)
add_linked_observer(user2, observer2)
expect(ObserverEnrollment.count).to eql 3
Enrollment.where(user_id: observer2, associated_user_id: user1).update_all(workflow_state: 'completed')
UserMerge.from(user1).into(user2)
expect(user1.observee_enrollments.size).to eql 1 # deleted
expect(user1.observee_enrollments.active_or_pending).to be_empty
expect(user2.observee_enrollments.size).to eql 2
expect(user2.observee_enrollments.active_or_pending.size).to eql 2
expect(observer1.observer_enrollments.active_or_pending.size).to eql 1
expect(observer2.observer_enrollments.active_or_pending.size).to eql 1
end
it "moves and uniquify observers" do
observer1 = user_model
observer2 = user_model
add_linked_observer(user1, observer1)
add_linked_observer(user1, observer2)
add_linked_observer(user2, observer2)
UserMerge.from(user1).into(user2)
data = UserMergeData.where(user_id: user2).first
expect(data.records.where(context_type: 'UserObservationLink').count).to eq 2
user1.reload
expect(user1.linked_observers).to be_empty
expect(UserObservationLink.where(:student => user1).first.workflow_state).to eq 'deleted'
user2.reload
expect(user2.linked_observers.sort_by(&:id)).to eql [observer1, observer2]
end
it "moves and uniquify observed users" do
student1 = user_model
student2 = user_model
student3 = user_model
add_linked_observer(student1, user1)
add_linked_observer(student2, user1)
add_linked_observer(student3, user1)
add_linked_observer(student2, user2)
# make sure active link from user 1 comes over even if user 2 has
# a destroyed link
link = add_linked_observer(student3, user2)
link.destroy
UserMerge.from(user1).into(user2)
user1.reload
expect(user1.linked_students).to be_empty
user2.reload
expect(user2.linked_students.sort_by(&:id)).to eql [student1, student2, student3]
end
it "moves conversations to the new user" do
c1 = user1.initiate_conversation([user_factory, user_factory]) # group conversation
c1.add_message("hello")
c1.update_attribute(:workflow_state, 'unread')
c2 = user1.initiate_conversation([user_factory]) # private conversation
c2.add_message("hello")
c2.update_attribute(:workflow_state, 'unread')
old_private_hash = c2.conversation.private_hash
UserMerge.from(user1).into(user2)
expect(c1.reload.user_id).to eql user2.id
expect(c1.conversation.participants).not_to include(user1)
expect(user1.reload.unread_conversations_count).to eql 0
expect(c2.reload.user_id).to eql user2.id
expect(c2.conversation.participants).not_to include(user1)
expect(c2.conversation.private_hash).not_to eql old_private_hash
expect(user2.reload.unread_conversations_count).to eql 2
end
it "points other user's observers to the new user" do
observer = user_model
course1.enroll_student(user1)
oe = course1.enroll_user(observer, 'ObserverEnrollment')
oe.update_attribute(:associated_user_id, user1.id)
UserMerge.from(user1).into(user2)
expect(oe.reload.associated_user_id).to eq user2.id
end
it "moves appointments" do
course1.enroll_user(user1, 'StudentEnrollment', :enrollment_state => 'active')
course1.enroll_user(user2, 'StudentEnrollment', :enrollment_state => 'active')
ag = AppointmentGroup.create(:title => "test",
:contexts => [course1],
:participants_per_appointment => 1,
:min_appointments_per_participant => 1,
:new_appointments => [
["#{Time.now.year + 1}-01-01 12:00:00", "#{Time.now.year + 1}-01-01 13:00:00"],
["#{Time.now.year + 1}-01-01 13:00:00", "#{Time.now.year + 1}-01-01 14:00:00"]
])
res1 = ag.appointments.first.reserve_for(user1, @teacher)
ag.appointments.last.reserve_for(user2, @teacher)
UserMerge.from(user1).into(user2)
res1.reload
expect(res1.context_id).to eq user2.id
expect(res1.context_code).to eq user2.asset_string
end
it "moves user attachments and handle duplicates" do
attachment1 = Attachment.create!(:user => user1, :context => user1, :filename => "test.txt", :uploaded_data => StringIO.new("first"))
attachment2 = Attachment.create!(:user => user1, :context => user1, :filename => "test.txt", :uploaded_data => StringIO.new("notfirst"))
attachment3 = Attachment.create!(:user => user2, :context => user2, :filename => "test.txt", :uploaded_data => StringIO.new("first"))
UserMerge.from(user1).into(user2)
run_jobs
expect(user2.attachments.count).to eq 2
expect(user2.attachments.not_deleted.count).to eq 2
expect(user2.attachments.not_deleted.detect { |a| a.md5 == attachment1.md5 }).to eq attachment3
new_attachment = user2.attachments.not_deleted.detect { |a| a.md5 == attachment2.md5 }
expect(new_attachment.display_name).not_to eq "test.txt" # attachment2 should be copied and renamed because it has unique file data
end
it "moves discussion topics and entries" do
topic = course1.discussion_topics.create!(user: user2)
entry = topic.discussion_entries.create!(user: user2)
UserMerge.from(user2).into(user1)
expect(topic.reload.user).to eq user1
expect(entry.reload.user).to eq user1
end
it "freshens moved topics" do
topic = course1.discussion_topics.create!(user: user2)
now = Time.at(5.minutes.from_now.to_i) # truncate milliseconds
Timecop.freeze(now) do
UserMerge.from(user2).into(user1)
expect(topic.reload.updated_at).to eq now
end
end
it "freshens topics with moved entries" do
topic = course1.discussion_topics.create!(user: user1)
topic.discussion_entries.create!(user: user2)
now = Time.at(5.minutes.from_now.to_i) # truncate milliseconds
Timecop.freeze(now) do
UserMerge.from(user2).into(user1)
expect(topic.reload.updated_at).to eq now
end
end
end
it "updates account associations" do
account1 = account_model
account2 = account_model
pseudo1 = (user1 = user_with_pseudonym :account => account1).pseudonym
pseudo2 = (user2 = user_with_pseudonym :account => account2).pseudonym
subsubaccount1 = (subaccount1 = account1.sub_accounts.create!).sub_accounts.create!
subsubaccount2 = (subaccount2 = account2.sub_accounts.create!).sub_accounts.create!
course_with_student(:account => subsubaccount1, :user => user1)
course_with_student(:account => subsubaccount2, :user => user2)
expect(user1.associated_accounts.map(&:id).sort).to eq [account1, subaccount1, subsubaccount1].map(&:id).sort
expect(user2.associated_accounts.map(&:id).sort).to eq [account2, subaccount2, subsubaccount2].map(&:id).sort
expect(pseudo1.user).to eq user1
expect(pseudo2.user).to eq user2
UserMerge.from(user1).into(user2)
pseudo1, pseudo2 = [pseudo1, pseudo2].map { |p| Pseudonym.find(p.id) }
user1, user2 = [user1, user2].map { |u| User.find(u.id) }
expect(pseudo1.user).to eq pseudo2.user
expect(pseudo1.user).to eq user2
expect(user1.associated_accounts.map(&:id).sort).to eq []
expect(user2.associated_accounts.map(&:id).sort).to eq [account1, account2, subaccount1, subaccount2, subsubaccount1, subsubaccount2].map(&:id).sort
end
context "versions" do
let!(:user1) { user_model }
let!(:user2) { user_model }
context "submissions" do
it "updates the versions table" do
other_user = user_model
a1 = assignment_model(:submission_types => 'online_text_entry')
a1.submit_homework(user2, {
:submission_type => 'online_text_entry',
:body => 'hi'
})
s1 = a1.submit_homework(user2, {
:submission_type => 'online_text_entry',
:body => 'hi again'
})
s_other = a1.submit_homework(other_user, {
:submission_type => 'online_text_entry',
:body => 'hi again'
})
expect(s1.versions.count).to eql(2)
s1.versions.each { |v| expect(v.model.user_id).to eql(user2.id) }
expect(s_other.versions.first.model.user_id).to eql(other_user.id)
UserMerge.from(user2).into(user1)
s1 = Submission.find(s1.id)
s_other.reload
expect(s1.versions.count).to eql(2)
s1.versions.each { |v| expect(v.model.user_id).to eql(user1.id) }
expect(s_other.versions.first.model.user_id).to eql(other_user.id)
end
it "updates the submission_versions table" do
assignment = assignment_model(submission_types: 'online_text_entry')
assignment.submit_homework(user2, {
submission_type: 'online_text_entry',
body: 'submission whoo'
})
submission = assignment.submit_homework(user2, {
submission_type: 'online_text_entry',
body: 'another submission!'
})
versions = SubmissionVersion.where(version_id: submission.versions)
expect(versions.count).to eql(2)
versions.each { |v| expect(v.user_id).to eql(user2.id) }
UserMerge.from(user2).into(user1)
versions.reload
expect(versions.count).to eql(2)
versions.each { |v| expect(v.user_id).to eql(user1.id) }
end
end
it "updates quiz submissions" do
quiz_with_graded_submission([], user: user2)
qs1 = @quiz_submission
quiz_with_graded_submission([], user: user2)
qs2 = @quiz_submission
expect(qs1.versions).to be_present
qs1.versions.each { |v| expect(v.model.user_id).to eql(user2.id) }
expect(qs2.versions).to be_present
qs2.versions.each { |v| expect(v.model.user_id).to eql(user2.id) }
UserMerge.from(user2).into(user1)
qs1.reload
qs2.reload
expect(qs1.versions).to be_present
qs1.versions.each { |v| expect(v.model.user_id).to eql(user1.id) }
expect(qs2.versions).to be_present
qs2.versions.each { |v| expect(v.model.user_id).to eql(user1.id) }
end
it "updates other appropriate versions" do
course_factory(active_all: true)
wiki_page = @course.wiki_pages.create(:title => "Hi", :user_id => user2.id)
ra = rubric_assessment_model(:context => @course, :user => user2)
expect(wiki_page.versions).to be_present
wiki_page.versions.each { |v| expect(v.model.user_id).to eql(user2.id) }
expect(ra.versions).to be_present
ra.versions.each { |v| expect(v.model.user_id).to eql(user2.id) }
UserMerge.from(user2).into(user1)
wiki_page.reload
ra.reload
expect(wiki_page.versions).to be_present
wiki_page.versions.each { |v| expect(v.model.user_id).to eql(user1.id) }
expect(ra.versions).to be_present
ra.versions.each { |v| expect(v.model.user_id).to eql(user1.id) }
end
end
context "sharding" do
specs_require_sharding
it 'moves past_lti_id to the new user on other shard' do
@shard1.activate do
account = Account.create!
@user1 = user_with_pseudonym(username: 'user1@example.com', active_all: 1, account: account)
end
course = course_factory(active_all: true)
user2 = user_with_pseudonym(username: 'user2@example.com', active_all: 1)
UserPastLtiId.create!(
user: user2,
context: course,
user_uuid: 'fake_uuid',
user_lti_id: 'fake_lti_id_from_old_merge'
)
UserMerge.from(user2).into(@user1)
expect(
UserPastLtiId.shard(course).where(user_id: @user1).take.user_lti_id
).to eq 'fake_lti_id_from_old_merge'
end
it 'moves prefs over with old format' do
@shard1.activate do
@user2 = user_model
account = Account.create!
@shard_course = course_factory(account: account)
@user2.preferences[:custom_colors] = { "course_#{@course.id}" => "#254284" }
end
course = course_factory
user1 = user_model
@user2.preferences[:custom_colors]["course_#{course.global_id}"] = "#346543"
@user2.save!
UserMerge.from(@user2).into(user1)
expect(user1.reload.preferences[:custom_colors].keys).to eq ["course_#{@shard_course.global_id}", "course_#{course.id}"]
end
it 'moves prefs over with new format' do
@shard1.activate do
@user2 = user_model
account = Account.create!
@shard_course = course_factory(account: account)
end
course = course_factory
user1 = user_model
@user2.set_preference(:custom_colors,
{ "course_#{@shard_course.local_id}" => "#254284", "course_#{course.global_id}" => "#346543" })
UserMerge.from(@user2).into(user1)
expect(user1.reload.get_preference(:custom_colors)).to eq(
{ "course_#{@shard_course.global_id}" => "#254284", "course_#{course.local_id}" => "#346543" }
)
end
it 'moves nicknames with old format' do
@shard1.activate do
@user2 = user_model
account = Account.create!
@shard_course = course_factory(account: account)
@user2.preferences[:course_nicknames] = { @shard_course.id => "Marketing" }
end
course = course_factory
user1 = user_model
@user2.preferences[:course_nicknames][course.global_id] = "Math"
@user2.save!
UserMerge.from(@user2).into(user1)
expect(user1.reload.preferences[:course_nicknames].keys).to eq [@shard_course.global_id, course.id]
end
it 'moves nicknames with new format' do
@shard1.activate do
@user2 = user_model
account = Account.create!
@shard_course = course_factory(account: account)
@user2.set_preference(:course_nicknames, @shard_course.id, "Marketing")
end
course = course_factory
user1 = user_model
@user2.set_preference(:course_nicknames, course.global_id, "Math")
@user2.save!
UserMerge.from(@user2).into(user1)
user1.reload
expect(user1.get_preference(:course_nicknames, @shard_course.global_id)).to eq "Marketing"
expect(user1.get_preference(:course_nicknames, course.id)).to eq "Math"
end
it 'handles favorites' do
@shard1.activate do
@user2 = user_model
account = Account.create!
@shard_course = course_factory(account: account)
@shard_course.enroll_user(@user2)
group = account.groups.create!
@fav = Favorite.create!(user: @user2, context: @shard_course)
@fav2 = Favorite.create!(user: @user2, context: group)
end
user1 = user_model
UserMerge.from(@user2).into(user1)
expect(user1.favorites.where(context_type: 'Course').take.context).to eq @shard_course
expect(user1.favorites.where(context_type: 'Group').count).to eq 1
end
it 'handles duplicate favorites' do
user2 = @shard1.activate do
user_model
end
user1 = user_model
course = course_factory
course.enroll_user(user1)
course.enroll_user(user2)
user1.favorites.create!(context: course)
user2.favorites.create!(context: course)
@shard1.activate do
UserMerge.from(user2).into(user1)
end
expect(user1.favorites.take.context_id).to eq course.id
end
it 'handles duplicate favorites other direction' do
user2 = @shard1.activate do
user_model
end
user1 = user_model
course = course_factory
course.enroll_user(user1)
course.enroll_user(user2)
user1.favorites.create!(context: course)
user2.favorites.create!(context: course)
@shard1.activate do
UserMerge.from(user1).into(user2)
end
expect(user2.favorites.take.context_id).to eq course.id
end
it 'merges with user_services across shards' do
user1 = user_model
@shard1.activate do
@user2 = user_model
user_service_model(user: @user2)
end
expect { UserMerge.from(@user2).into(user1) }.to_not raise_error
end
it "merges a user across shards" do
user1 = user_with_pseudonym(:username => 'user1@example.com', :active_all => 1)
p1 = @pseudonym
cc1 = @cc
@shard1.activate do
account = Account.create!
@user2 = user_with_pseudonym(:username => 'user2@example.com', :active_all => 1, :account => account)
@p2 = @pseudonym
end
@shard2.activate do
UserMerge.from(user1).into(@user2)
end
expect(user1).to be_deleted
expect(p1.reload.user).to eq @user2
expect(cc1.reload).to be_retired
@user2.reload
expect(@user2.communication_channels.to_a.map(&:path).sort).to eq ['user1@example.com', 'user2@example.com']
expect(@user2.all_pseudonyms).to eq [p1, @p2]
expect(@user2.associated_shards).to eq [@shard1, Shard.default]
end
it 'handles root_account_ids on ccs' do
user1 = user_with_pseudonym(username: 'user1@example.com', active_all: 1)
other_account = Account.create(name: 'anuroot')
UserAccountAssociation.create!(account: other_account, user: user1)
user1.update_root_account_ids
user2 = user_with_pseudonym(username: 'user2@example.com', active_all: 1, account: other_account)
UserMerge.from(user2).into(user1)
expect(@cc.reload.root_account_ids).to eq user1.root_account_ids
end
it "associates the user with all shards" do
user1 = user_with_pseudonym(:username => 'user1@example.com', :active_all => 1)
p1 = @pseudonym
@shard1.activate do
account = Account.create!
@p2 = account.pseudonyms.create!(:unique_id => 'user1@exmaple.com', :user => user1)
end
@shard2.activate do
account = Account.create!
@user2 = user_with_pseudonym(:username => 'user2@example.com', :active_all => 1, :account => account)
@p3 = @pseudonym
UserMerge.from(user1).into(@user2)
end
expect(@user2.associated_shards.sort_by(&:id)).to eq [Shard.default, @shard1, @shard2].sort_by(&:id)
expect(@user2.all_pseudonyms.sort_by(&:id)).to eq [p1, @p2, @p3].sort_by(&:id)
end
it "moves ccs to the new user (but only if they don't already exist)" do
user1 = user_model
@shard1.activate do
@user2 = user_model
end
# unconfirmed => active conflict
communication_channel(user1, { username: 'a@instructure.com' })
communication_channel(@user2, { username: 'A@instructure.com', active_cc: true })
# active => unconfirmed conflict
communication_channel(user1, { username: 'b@instructure.com', active_cc: true })
communication_channel(@user2, { username: 'B@instructure.com' })
# active => active conflict
communication_channel(user1, { username: 'c@instructure.com', active_cc: true })
communication_channel(@user2, { username: 'C@instructure.com', active_cc: true })
# unconfirmed => unconfirmed conflict
communication_channel(user1, { username: 'd@instructure.com' })
communication_channel(@user2, { username: 'D@instructure.com' })
# retired => unconfirmed conflict
communication_channel(user1, { username: 'e@instructure.com', cc_state: 'retired' })
communication_channel(@user2, { username: 'E@instructure.com' })
# unconfirmed => retired conflict
communication_channel(user1, { username: 'f@instructure.com' })
communication_channel(@user2, { username: 'F@instructure.com', cc_state: 'retired' })
# retired => active conflict
communication_channel(user1, { username: 'g@instructure.com', cc_state: 'retired' })
communication_channel(@user2, { username: 'G@instructure.com', cc_state: 'active' })
# active => retired conflict
communication_channel(user1, { username: 'h@instructure.com', cc_state: 'active' })
communication_channel(@user2, { username: 'H@instructure.com', cc_state: 'retired' })
# retired => retired conflict
communication_channel(user1, { username: 'i@instructure.com', cc_state: 'retired' })
communication_channel(@user2, { username: 'I@instructure.com', cc_state: 'retired' })
# <nothing> => active
communication_channel(@user2, { username: 'j@instructure.com', active_cc: true })
# active => <nothing>
communication_channel(user1, { username: 'k@instructure.com', active_cc: true })
# <nothing> => unconfirmed
communication_channel(@user2, { username: 'l@instructure.com' })
# unconfirmed => <nothing>
communication_channel(user1, { username: 'm@instructure.com' })
# <nothing> => retired
communication_channel(@user2, { username: 'n@instructure.com', cc_state: 'retired' })
# retired => <nothing>
communication_channel(user1, { username: 'o@instructure.com', cc_state: 'retired' })
@shard2.activate do
UserMerge.from(user1).into(@user2)
end
user1.reload
@user2.reload
expect(@user2.communication_channels.map { |cc| [cc.path, cc.workflow_state] }.sort).to match_array([
['A@instructure.com', 'active'],
['C@instructure.com', 'active'],
['D@instructure.com', 'unconfirmed'],
['E@instructure.com', 'unconfirmed'],
['G@instructure.com', 'active'],
['I@instructure.com', 'retired'],
['b@instructure.com', 'active'],
['f@instructure.com', 'unconfirmed'],
['h@instructure.com', 'active'],
['j@instructure.com', 'active'],
['k@instructure.com', 'active'],
['l@instructure.com', 'unconfirmed'],
['m@instructure.com', 'unconfirmed'],
['n@instructure.com', 'retired'],
['o@instructure.com', 'retired']
])
# on cross shard merges, the deleted user retains all CCs (pertinent ones were
# duplicated over to the surviving shard)
expect(user1.communication_channels.map { |cc| [cc.path, cc.workflow_state] }.sort).to match_array([
['a@instructure.com', 'retired'],
['b@instructure.com', 'retired'],
['c@instructure.com', 'retired'],
['d@instructure.com', 'retired'],
['e@instructure.com', 'retired'],
['f@instructure.com', 'retired'],
['g@instructure.com', 'retired'],
['h@instructure.com', 'retired'],
['i@instructure.com', 'retired'],
['k@instructure.com', 'retired'],
['m@instructure.com', 'retired'],
['o@instructure.com', 'retired']
])
end
it "does not fail copying retired sms channels" do
user1 = User.create!
@shard1.activate do
@user2 = User.create!
end
cc1 = @user2.communication_channels.sms.create!(:path => 'abc')
cc1.retire!
@user2.reload
UserMerge.from(@user2).into(user1)
expect(user1.communication_channels.reload.length).to eq 1
cc2 = user1.communication_channels.first
expect(cc2.path).to eq 'abc'
expect(cc2.workflow_state).to eq 'retired'
end
it "moves user attachments and handle duplicates" do
course_factory
# FileSystemBackend is not namespace-aware, so the same id+name in
# different shards (e.g. root_attachment and its copy) can cause
# :boom: ... set high ids for things that get copied, so their
# copies' ids don't collide
root_attachment = Attachment.create(:id => 1_000_000, :context => @course, :filename => "unique_name1.txt",
:uploaded_data => StringIO.new("root_attachment_data"))
user1 = User.create!
# should not copy because it's identical to @user2_attachment1
user1_attachment1 = Attachment.create!(:user => user1, :context => user1, :filename => "shared_name1.txt",
:uploaded_data => StringIO.new("shared_data"))
# copy should have root_attachment directed to @user2_attachment2, and be renamed
user1_attachment2 = Attachment.create!(:id => 1_000_001, :user => user1, :context => user1, :filename => "shared_name2.txt",
:uploaded_data => StringIO.new("shared_data2"))
# should copy as a root_attachment (even though it isn't one currently)
user1_attachment3 = Attachment.create!(:id => 1_000_002, :user => user1, :context => user1, :filename => "unique_name2.txt",
:uploaded_data => StringIO.new("root_attachment_data"))
user1_attachment3.content_type = "text/plain"
user1_attachment3.save!
expect(user1_attachment3.root_attachment).to eq root_attachment
@shard1.activate do
new_account = Account.create!
@user2 = user_with_pseudonym(:account => new_account)
@user2_attachment1 = Attachment.create!(:user => @user2, :context => @user2, :filename => "shared_name1.txt",
:uploaded_data => StringIO.new("shared_data"))
@user2_attachment2 = Attachment.create!(:user => @user2, :context => @user2, :filename => "unique_name3.txt",
:uploaded_data => StringIO.new("shared_data2"))
@user2_attachment3 = Attachment.create!(:user => @user2, :context => @user2, :filename => "shared_name2.txt",
:uploaded_data => StringIO.new("unique_data"))
end
UserMerge.from(user1).into(@user2)
run_jobs
# 3 from user1, and 3 from @user2
expect(@user2.attachments.not_deleted.count).to eq 6
new_user2_attachment1 = @user2.attachments.not_deleted.detect { |a| a.md5 == user1_attachment2.md5 && a.id != @user2_attachment2.id }
expect(new_user2_attachment1.root_attachment).to eq @user2_attachment2
expect(new_user2_attachment1.display_name).not_to eq user1_attachment2.display_name # should rename
expect(new_user2_attachment1.namespace).not_to eq user1_attachment1.namespace
new_user2_attachment2 = @user2.attachments.not_deleted.detect { |a| a.md5 == user1_attachment3.md5 }
expect(new_user2_attachment2.root_attachment).to be_nil
expect(new_user2_attachment2.content_type).to eq "text/plain"
end
it "marks cross-shard user submission attachments so they're still visible" do
user1 = User.create!
user1_attachment = Attachment.create!(:user => user1, :context => user1, :filename => "shared_name1.txt",
:uploaded_data => StringIO.new("shared_data"))
course_factory
a1 = assignment_model(:submission_types => "online_upload")
submission = a1.submit_homework(user1, attachments: [user1_attachment])
@shard1.activate do
new_account = Account.create!
@user2 = user_with_pseudonym(:account => new_account)
end
UserMerge.from(user1).into(@user2)
run_jobs
expect(Submission.find(submission.id).versioned_attachments).to eq [user1_attachment]
end
it "moves cross-sharded conversations to the new user" do
user1 = user_factory
c1 = user1.initiate_conversation([user_factory, user_factory]) # group conversation
c1.add_message("hello")
c1.update_attribute(:workflow_state, 'unread')
c2 = user1.initiate_conversation([user_factory]) # private conversation
c2.add_message("hello")
c2.update_attribute(:workflow_state, 'unread')
@shard1.activate do
new_account = Account.create!
@user2 = user_with_pseudonym(:account => new_account)
end
c3 = user1.initiate_conversation([user_factory, @user2]) # conversation where the target user already exists
c3.add_message("hello")
UserMerge.from(user1).into(@user2)
expect(@user2.all_conversations.pluck(:conversation_id)).to match_array([c1, c2, c3].map(&:conversation_id))
end
context "manual invitation" do
it "does not keep a temporary invitation in cache for an enrollment deleted after a user merge" do
set_cache(:redis_cache_store)
email = 'foo@example.com'
course_factory
@course.offer!
# create an active enrollment (usually through an SIS import)
user1 = user_with_pseudonym(:username => email, :active_all => true)
@course.enroll_user(user1).accept!
# manually invite the same email address into the course
# if open_registration is set on the root account, this creates a new temporary user
user2 = user_with_communication_channel(:username => email, :user_state => "creation_pending")
@course.enroll_user(user2)
# cache the temporary invitations
expect(Enrollment.cached_temporary_invitations(user1.communication_channels.first.path)).not_to be_empty
# when the user follows the confirmation link, they will be prompted to merge into the other user
UserMerge.from(user2).into(user1)
# should not hold onto the now-deleted invitation
# (otherwise it will retrieve it in CoursesController#fetch_enrollment,
# which causes the login loop in CoursesController#accept_enrollment)
expect(Enrollment.cached_temporary_invitations(user1.reload.communication_channels.first.path)).to be_empty
end
end
end
end