canvas-lms/spec/lib/user_search_spec.rb

607 lines
25 KiB
Ruby

# frozen_string_literal: true
# Copyright (C) 2012 - 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 UserSearch do
describe ".for_user_in_context" do
let(:search_names) { ["Rose Tyler", "Martha Jones", "Rosemary Giver", "Martha Stewart", "Tyler Pickett", "Jon Stewart", "Stewart Little", "Ĭńşŧřůćƭǜȑȩ Person"] }
let(:course) { Course.create!(workflow_state: "available") }
let(:users) { UserSearch.for_user_in_context("Stewart", course, user, nil, sort: "username", order: "asc").to_a }
let(:names) { users.map(&:name) }
let(:user) { User.last }
let(:student) { User.where(name: search_names.last).first }
before do
teacher = User.create!(name: "Tyler Teacher")
TeacherEnrollment.create!(user: teacher, course:, workflow_state: "active")
search_names.each do |name|
student = User.create!(name:)
StudentEnrollment.create!(user: student, course:, workflow_state: "active")
end
User.create!(name: "admin")
TeacherEnrollment.create!(user:, course:, workflow_state: "active")
end
describe "when excluding a group" do
it "does not include users in that group" do
group = Group.create! name: "test", context: course
group.add_user user
expect(UserSearch.for_user_in_context("admin", course, user, nil, exclude_groups: [group.id]).size).to eq 0
end
end
describe "with complex search enabled" do
before { Setting.set("user_search_with_full_complexity", "true") }
describe "with gist setting enabled" do
before { Setting.set("user_search_with_gist", "true") }
it "searches case-insensitively" do
expect(UserSearch.for_user_in_context("steWArt", course, user).size).to eq 3
end
it "uses postgres lower(), not ruby downcase()" do
# ruby 1.9 downcase doesn't handle the downcasing of many multi-byte characters correctly
expect(UserSearch.for_user_in_context("Ĭńşŧřůćƭǜȑȩ", course, user).size).to eq 1
end
it "returns an enumerable" do
expect(users.size).to eq 3
end
it "contains the matching users" do
expect(names).to include("Martha Stewart")
expect(names).to include("Stewart Little")
expect(names).to include("Jon Stewart")
end
it "does not contain users I am not allowed to see" do
unenrolled_user = User.create!(name: "Unenrolled User")
search_results = UserSearch.for_user_in_context("Stewart", course, unenrolled_user).map(&:name)
expect(search_results).to eq []
end
it "will not pickup students outside the course" do
User.create!(name: "Stewart Stewart")
# names is evaluated lazily from the 'let' block so ^ user is still being
# created before the query executes
expect(names).not_to include("Stewart Stewart")
end
it "will find teachers" do
results = UserSearch.for_user_in_context("Tyler", course, user)
expect(results.map(&:name)).to include("Tyler Teacher")
end
it "sorts by name" do
expect(names.size).to eq 3
expect(names[0]).to eq "Stewart Little"
expect(names[1]).to eq "Jon Stewart"
expect(names[2]).to eq "Martha Stewart"
end
describe "filtering by role" do
subject { names }
describe "to a single role" do
let(:users) { UserSearch.for_user_in_context("Tyler", course, user, nil, enrollment_type: "student").to_a }
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
it { is_expected.not_to include("Tyler Teacher") }
end
describe "to multiple roles" do
let(:users) { UserSearch.for_user_in_context("Tyler", course, student, nil, enrollment_type: ["ta", "teacher"]).to_a }
before do
ta = User.create!(name: "Tyler TA")
TaEnrollment.create!(user: ta, course:, workflow_state: "active")
end
it { is_expected.to include("Tyler TA") }
it { is_expected.to include("Tyler Teacher") }
it { is_expected.not_to include("Rose Tyler") }
end
describe "FooEnrollment names" do
let(:users) { UserSearch.for_user_in_context("Tyler", course, user, nil, enrollment_type: "StudentEnrollment").to_a }
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
it { is_expected.not_to include("Tyler Teacher") }
end
describe "with the broader role parameter" do
let(:users) { UserSearch.for_user_in_context("Tyler", course, student, nil, enrollment_role: "ObserverEnrollment").to_a }
before do
ta = User.create!(name: "Tyler Observer")
ObserverEnrollment.create!(user: ta, course:, workflow_state: "active")
ta2 = User.create!(name: "Tyler Observer 2")
ObserverEnrollment.create!(user: ta2, course:, workflow_state: "active")
add_linked_observer(student, ta2)
end
it { is_expected.not_to include("Tyler Observer 2") }
it { is_expected.not_to include("Tyler Observer") }
it { is_expected.not_to include("Tyler Teacher") }
it { is_expected.not_to include("Rose Tyler") }
end
describe "with the role name parameter" do
before do
newstudent = User.create!(name: "Tyler Student")
StudentEnrollment.create!(user: newstudent, course:, workflow_state: "active")
end
describe "when the context is a course" do
let(:users) { UserSearch.for_user_in_context("Tyler", course, user, nil, enrollment_role: "StudentEnrollment").to_a }
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
it { is_expected.to include("Tyler Student") }
it { is_expected.not_to include("Tyler Teacher") }
end
describe "when the context is an account" do
let(:users) { UserSearch.for_user_in_context("Tyler", course.account, user, nil, enrollment_role: "StudentEnrollment").to_a }
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
it { is_expected.to include("Tyler Student") }
it { is_expected.not_to include("Tyler Teacher") }
end
end
describe "with the role id parameter" do
let(:users) { UserSearch.for_user_in_context("Tyler", course, student, nil, enrollment_role_id: student_role.id).to_a }
before do
newstudent = User.create!(name: "Tyler Student")
StudentEnrollment.create!(user: newstudent, course:, workflow_state: "active")
end
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
it { is_expected.to include("Tyler Student") }
it { is_expected.not_to include("Tyler Teacher") }
end
end
describe "searching on logins" do
let(:pseudonym) { user.pseudonyms.build }
before do
pseudonym.sis_user_id = "SOME_SIS_ID"
pseudonym.unique_id = "SOME_UNIQUE_ID@example.com"
pseudonym.integration_id = "ACME_123"
pseudonym.current_login_at = Time.utc(2019, 11, 11)
pseudonym.save!
end
it "will match against an sis id" do
expect(UserSearch.for_user_in_context("SOME_SIS", course, user)).to eq [user]
end
it "will match against an integration id" do
expect(UserSearch.for_user_in_context("ACME", course, user)).to eq [user]
end
describe "will match against a suspended user" do
before do
pseudonym.workflow_state = "suspended"
pseudonym.save!
end
it "by sis id" do
expect(UserSearch.for_user_in_context("SOME_SIS", course, user)).to eq [user]
end
it "by integrtion id" do
expect(UserSearch.for_user_in_context("ACME", course, user)).to eq [user]
end
it "by user name" do
expect(UserSearch.for_user_in_context("admin", course, user)).to eq [user]
end
end
it "will not match against a sis id without :read_sis permission" do
RoleOverride.create!(context: Account.default,
role: teacher_role,
permission: "read_sis",
enabled: false)
expect(UserSearch.for_user_in_context("SOME_SIS", course, user)).to eq []
end
it "will not match against an integration id without :read_sis permission" do
RoleOverride.create!(context: Account.default,
role: teacher_role,
permission: "read_sis",
enabled: false)
expect(UserSearch.for_user_in_context("ACME", course, user)).to eq []
end
it "will match against an sis id and regular id" do
user2 = User.create(name: "user two")
pseudonym.sis_user_id = user2.id.to_s
pseudonym.save!
course.enroll_user(user2)
expect(UserSearch.for_user_in_context(user2.id.to_s, course, user)).to eq [user, user2]
end
it "handles search terms out of bounds for max bigint" do
pseudonym.sis_user_id = "9223372036854775808"
pseudonym.save!
expect(UserSearch.for_user_in_context("9223372036854775808", course, user)).to eq [user]
end
it "will match against a login id" do
expect(UserSearch.for_user_in_context("UNIQUE_ID", course, user)).to eq [user]
end
it "will not search login id without permission" do
RoleOverride.create!(context: Account.default,
role: teacher_role,
permission: "view_user_logins",
enabled: false)
expect(UserSearch.for_user_in_context("UNIQUE_ID", course, user)).to eq []
end
it "returns the last_login column when searching and sorting" do
results = UserSearch.for_user_in_context("UNIQUE_ID", course, user, nil, sort: "last_login")
expect(results.first.read_attribute("last_login")).to eq(Time.utc(2019, 11, 11))
end
it "can match an SIS id and a user name in the same query" do
pseudonym.sis_user_id = "MARTHA_SIS_ID"
pseudonym.save!
other_user = User.where(name: "Martha Stewart").first
results = UserSearch.for_user_in_context("martha", course, user)
expect(results).to include(user)
expect(results).to include(other_user)
end
it "sorts by sis id" do
User.find_by(name: "Rose Tyler").pseudonyms.create!(unique_id: "rose.tyler@example.com",
sis_user_id: "25rose",
account_id: course.root_account_id)
User.find_by(name: "Tyler Pickett").pseudonyms.create!(unique_id: "tyler.pickett@example.com",
sis_user_id: "1tyler",
account_id: course.root_account_id)
users = UserSearch.for_user_in_context("Tyler", course, user, nil, sort: "sis_id")
expect(users.map(&:name)).to eq ["Tyler Pickett", "Rose Tyler", "Tyler Teacher"]
end
it "sorts by integration id" do
User.find_by(name: "Rose Tyler").pseudonyms.create!(unique_id: "rose.tyler@example.com",
integration_id: "25rose",
account_id: course.root_account_id)
User.find_by(name: "Tyler Pickett").pseudonyms.create!(unique_id: "tyler.pickett@example.com",
integration_id: "1tyler",
account_id: course.root_account_id)
users = UserSearch.for_user_in_context("Tyler", course, user, nil, sort: "integration_id")
expect(users.map(&:name)).to eq ["Tyler Pickett", "Rose Tyler", "Tyler Teacher"]
end
it "does not return users twice if it matches their name and an old login" do
tyler = User.find_by(name: "Tyler Pickett")
tyler.pseudonyms.create!(unique_id: "Yo", account_id: course.root_account_id, current_login_at: Time.zone.now)
tyler.pseudonyms.create!(unique_id: "Pickett", account_id: course.root_account_id, current_login_at: 1.week.ago)
users = UserSearch.for_user_in_context("Pickett", course, user, nil, sort: "username")
expect(users.map(&:name)).to eq ["Tyler Pickett"]
end
end
describe "searching on emails" do
let(:user1) { user_with_pseudonym(user:) }
let(:cc) { communication_channel(user1, { username: "the.giver@example.com" }) }
before do
cc.confirm!
end
it "matches against an email" do
expect(UserSearch.for_user_in_context("the.giver", course, user)).to eq [user]
end
it "requires :read_email_addresses permission" do
RoleOverride.create!(context: Account.default,
role: teacher_role,
permission: "read_email_addresses",
enabled: false)
expect(UserSearch.for_user_in_context("the.giver", course, user)).to eq []
end
it "can match an email and a name in the same query" do
results = UserSearch.for_user_in_context("giver", course, user)
expect(results).to include(user)
expect(results).to include(User.where(name: "Rosemary Giver").first)
end
it "will not match channels where the type is not email" do
cc.update!(path_type: CommunicationChannel::TYPE_TWITTER)
expect(UserSearch.for_user_in_context("the.giver", course, user)).to eq []
end
it "doesn't match retired channels" do
cc.retire!
expect(UserSearch.for_user_in_context("the.giver", course, user)).to eq []
end
it "matches unconfirmed channels", priority: 1 do
communication_channel(user, { username: "unconfirmed@example.com" })
expect(UserSearch.for_user_in_context("unconfirmed", course, user)).to eq [user]
end
it "sorts by email" do
communication_channel(User.find_by(name: "Tyler Pickett"), { username: "1tyler@example.com" })
communication_channel(User.find_by(name: "Tyler Teacher"), { username: "25teacher@example.com" })
users = UserSearch.for_user_in_context("Tyler", course, user, nil, sort: "email")
expect(users.map(&:name)).to eq ["Tyler Pickett", "Tyler Teacher", "Rose Tyler"]
end
end
describe "searching by a DB ID" do
it "matches against the database id" do
expect(UserSearch.for_user_in_context(user.id, course, user)).to eq [user]
end
it "matches against a database id and a user simultaneously" do
other_user = student_in_course(course:, name: user.id.to_s).user
expect(UserSearch.for_user_in_context(user.id, course, user)).to match_array [user, other_user]
end
describe "cross-shard users" do
specs_require_sharding
it "matches against the database id of a cross-shard user" do
user = @shard1.activate { user_model }
course.enroll_student(user)
expect(UserSearch.for_user_in_context(user.global_id, course, user)).to eq [user]
expect(UserSearch.for_user_in_context(user.global_id, course.account, user)).to eq [user]
end
end
end
end
describe "with gist setting disabled" do
before { Setting.set("user_search_with_gist", "false") }
it "returns a list of matching users using a prefix search" do
expect(names).to eq ["Stewart Little"]
end
end
end
describe "account user search with search term" do
subject { names }
before { Setting.set("user_search_with_full_complexity", "true") }
let(:course1) { Course.create!(workflow_state: "available") }
let(:user_names_not_enrolled) { ["not enrolled Tyler 01", "not enrolled 02"] }
let(:user_names_enrolled_in_course1) { ["enrolled 01", "enrolled Tyler 02"] }
let(:teacher_names_enrolled_in_course1) { ["enrolled teacher Tyler 01", "enrolled teacher 02"] }
before do
user_names_not_enrolled.each do |name|
User.create!(name:)
end
user_names_enrolled_in_course1.each do |name|
student = User.create!(name:)
StudentEnrollment.create!(user: student, course: course1, workflow_state: "active")
end
teacher_names_enrolled_in_course1.each do |name|
teacher = User.create!(name:)
TeacherEnrollment.create!(user: teacher, course: course1, workflow_state: "active")
end
end
describe "to a single role" do
let(:users) { UserSearch.for_user_in_context("Tyler", course.account, user, nil, enrollment_type: "student").to_a }
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
# include students from different courses
it { is_expected.to include("enrolled Tyler 02") }
# don't include teachers
it { is_expected.not_to include("enrolled teacher Tyler 01") }
# don't include users not enrolled
it { is_expected.not_to include("not enrolled Tyler 01") }
end
describe "to multiple roles" do
let(:users) { UserSearch.for_user_in_context("Tyler", course.account, user, nil, enrollment_type: ["student", "teacher"]).to_a }
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
# include students from different courses
it { is_expected.to include("enrolled Tyler 02") }
# include teachers
it { is_expected.to include("enrolled teacher Tyler 01") }
# don't include users not enrolled
it { is_expected.not_to include("not enrolled Tyler 01") }
end
end
describe "with complex search disabled" do
before do
Setting.set("user_search_with_full_complexity", "false")
Setting.set("user_search_with_gist", "true")
end
it "matches against the display name" do
expect(users.size).to eq 3
end
it "does not match against sis ids" do
pseudonym = user.pseudonyms.build
pseudonym.sis_user_id = "SOME_SIS_ID"
pseudonym.unique_id = "SOME_UNIQUE_ID@example.com"
pseudonym.save!
expect(UserSearch.for_user_in_context("SOME_SIS", course, user)).to eq []
end
it "does not match against integration ids" do
pseudonym = user.pseudonyms.build
pseudonym.unique_id = "SOME_UNIQUE_ID@example.com"
pseudonym.integration_id = "ACME_123"
pseudonym.save!
expect(UserSearch.for_user_in_context("ACME", course, user)).to eq []
end
it "does not match against emails" do
communication_channel(user, { username: "the.giver@example.com" })
expect(UserSearch.for_user_in_context("the.giver", course, user)).to eq []
end
end
end
describe ".like_string_for" do
it "uses a prefix if gist is not configured" do
Setting.set("user_search_with_gist", "false")
expect(UserSearch.like_string_for("word")).to eq "word%"
end
it "modulos both sides if gist is configured" do
Setting.set("user_search_with_gist", "true")
expect(UserSearch.like_string_for("word")).to eq "%word%"
end
end
describe ".scope_for" do
let(:search_names) do
["Rose Tyler",
"Martha Jones",
"Rosemary Giver",
"Martha Stewart",
"Tyler Pickett",
"Jon Stewart",
"Stewart Little",
"Ĭńşŧřůćƭǜȑȩ Person"]
end
let(:course) { Course.create!(workflow_state: "available") }
let(:users) { UserSearch.scope_for(course, user, sort: "username", order: "desc").to_a }
let(:names) { users.map(&:name) }
let(:user) { User.last }
let(:student) { User.where(name: search_names.last).first }
before do
search_names.each do |name|
student = User.create!(name:)
StudentEnrollment.create!(user: student, course:, workflow_state: "active")
end
end
it "sorts by name desc" do
expect(names.size).to eq 8
expect(names[0]).to eq "Rose Tyler"
expect(names[1]).to eq "Martha Stewart"
expect(names[2]).to eq "Jon Stewart"
expect(names[3]).to eq "Tyler Pickett"
expect(names[4]).to eq "Ĭńşŧřůćƭǜȑȩ Person"
expect(names[5]).to eq "Stewart Little"
expect(names[6]).to eq "Martha Jones"
expect(names[7]).to eq "Rosemary Giver"
end
it "raises an error if there is a bad enrollment type" do
course = Course.create!
student = User.create!
bad_scope = -> { UserSearch.scope_for(course, student, enrollment_type: "all") }
expect(&bad_scope).to raise_error(RequestError, "Invalid enrollment type: all")
end
it "doesn't explode with group context" do
course_with_student
group = @course.groups.create!
group.add_user(@student)
account_admin_user
expect(UserSearch.scope_for(group, @admin, enrollment_type: ["student"], include_inactive_enrollments: true).to_a).to eq [@student]
expect(UserSearch.scope_for(group, @admin, enrollment_type: ["teacher"]).to_a).to be_empty
end
describe "account user list filtering by role" do
subject { names }
let(:course1) { Course.create!(workflow_state: "available") }
let(:user_names_not_enrolled) { ["not enrolled 01", "not enrolled 02"] }
let(:user_names_enrolled_in_course1) { ["enrolled 01", "enrolled 02"] }
let(:teacher_names_enrolled_in_course1) { ["enrolled teacher 01", "enrolled teacher 02"] }
before do
user_names_not_enrolled.each do |name|
User.create!(name:)
end
user_names_enrolled_in_course1.each do |name|
student = User.create!(name:)
StudentEnrollment.create!(user: student, course: course1, workflow_state: "active")
end
teacher_names_enrolled_in_course1.each do |name|
teacher = User.create!(name:)
TeacherEnrollment.create!(user: teacher, course: course1, workflow_state: "active")
end
end
describe "to a single role" do
let(:users) { UserSearch.scope_for(course.account, nil, enrollment_type: "student").to_a }
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
# include students from different courses
it { is_expected.to include("enrolled 01") }
it { is_expected.to include("enrolled 02") }
# don't include teachers
it { is_expected.not_to include("enrolled teacher 01") }
it { is_expected.not_to include("enrolled teacher 02") }
# don't include users not enrolled
it { is_expected.not_to include("not enrolled 01") }
it { is_expected.not_to include("not enrolled 02") }
end
describe "to multiple roles" do
let(:users) { UserSearch.scope_for(course.account, nil, enrollment_type: ["student", "teacher"]).to_a }
it { is_expected.to include("Rose Tyler") }
it { is_expected.to include("Tyler Pickett") }
# include students from different courses
it { is_expected.to include("enrolled 01") }
it { is_expected.to include("enrolled 02") }
# include teachers
it { is_expected.to include("enrolled teacher 01") }
it { is_expected.to include("enrolled teacher 02") }
# don't include users not enrolled
it { is_expected.not_to include("not enrolled 01") }
it { is_expected.not_to include("not enrolled 02") }
end
end
end
end