otwarchive-symphonyarchive/spec/models/user_spec.rb
2026-03-11 22:22:11 +00:00

390 lines
13 KiB
Ruby

require "spec_helper"
describe User do
describe "validations" do
context "with a forbidden user name" do
let(:forbidden_username) { Faker::Lorem.characters(number: 8) }
before do
allow(ArchiveConfig).to receive(:FORBIDDEN_USERNAMES).and_return([forbidden_username])
end
it { is_expected.not_to allow_values(forbidden_username, forbidden_username.swapcase).for(:login) }
it "does not prevent saving when the name is unchanged" do
existing_user = build(:user, login: forbidden_username)
existing_user.save!(validate: false)
expect(existing_user.save).to be_truthy
end
end
end
describe "#destroy" do
context "on a user with kudos" do
let(:user) { create(:user) }
let!(:kudo_bundle) { create_list(:kudo, 2, user: user) }
it "removes user from kudos" do
user.destroy!
kudo_bundle.each do |kudo|
kudo.reload
expect(kudo.user).to be_nil
expect(kudo.user_id).to be_nil
end
end
end
context "when the user has a fnok" do
let(:fnok) { create(:fannish_next_of_kin) }
let(:user) { fnok.user }
let(:kin) { fnok.kin }
it "logs the fnok removal on the kin side" do
user_id = user.id
user.destroy!
log_item = kin.reload.log_items.last
expect(log_item.action).to eq(ArchiveConfig.ACTION_REMOVED_AS_FNOK)
expect(log_item.fnok_user_id).to eq(user_id)
expect(log_item.admin_id).to be_nil
expect(log_item.note).to eq("System Generated")
end
end
context "when the user is set as someone else's fnok" do
let(:fnok) { create(:fannish_next_of_kin) }
let(:user) { fnok.kin }
let(:person) { fnok.user }
it "removes the relationship and creates a log item of the removal" do
user_id = user.id
user.destroy!
expect(person.reload.fannish_next_of_kin).to be_nil
log_item = person.log_items.last
expect(log_item.action).to eq(ArchiveConfig.ACTION_REMOVE_FNOK)
expect(log_item.fnok_user_id).to eq(user_id)
expect(log_item.admin_id).to be_nil
expect(log_item.note).to eq("System Generated")
end
end
end
describe "#save" do
context "on a valid user" do
let(:user) { build(:user) }
it "saves without errors" do
expect(user.save).to be_truthy
end
it "encrypts password" do
user.save
expect(user.encrypted_password).not_to be_empty
expect(user.encrypted_password).not_to eq(user.password)
end
it "creates default associations" do
user.save
expect(user.profile).not_to be_nil
expect(user.preference).not_to be_nil
expect(user.pseuds.size).to eq(1)
expect(user.pseuds.first.name).to eq(user.login)
expect(user.pseuds.first.is_default).to be_truthy
end
end
describe "on an invalid user" do
context "missing the age_over_13 flag" do
let(:no_age_over_13) { build(:user, age_over_13: "0") }
it "does not save" do
expect(no_age_over_13.save).to be_falsey
expect(no_age_over_13.errors[:age_over_13].first).to include("you have to be over 13!")
end
end
context "missing data_processing flag" do
let(:no_data_processing) { build(:user, data_processing: "0") }
it "does not save" do
expect(no_data_processing.save).to be_falsey
expect(no_data_processing.errors[:data_processing].first).to include("you need to consent to the processing of your personal data")
end
end
context "missing the terms_of_service flag" do
let(:no_tos) { build(:user, terms_of_service: "0") }
it "does not save" do
expect(no_tos.save).to be_falsey
expect(no_tos.errors[:terms_of_service].first).to include("you need to accept the Terms")
end
end
context "with login too short" do
let(:login_short) { build(:user, login: Faker::Lorem.characters(number: ArchiveConfig.LOGIN_LENGTH_MIN - 1)) }
it "does not save" do
expect(login_short.save).to be_falsey
expect(login_short.errors[:login].first).to include("is too short")
end
end
context "with login too long" do
let(:login_long) { build(:user, login: Faker::Lorem.characters(number: ArchiveConfig.LOGIN_LENGTH_MAX + 1)) }
it "does not save" do
expect(login_long.save).to be_falsey
expect(login_long.errors[:login].first).to include("is too long")
end
end
BAD_EMAILS.each do |email|
context "with email #{email}" do
let(:bad_email) { build(:user, email: email) }
it "does not save" do
expect(bad_email.save).to be_falsey
expect(bad_email.errors[:email]).to include("should look like an email address.")
end
end
end
context "with password too short" do
let(:password_short) { build(:user, password: Faker::Lorem.characters(number: ArchiveConfig.PASSWORD_LENGTH_MIN - 1)) }
it "does not save" do
expect(password_short.save).to be_falsey
expect(password_short.errors[:password].first).to include("is too short")
end
end
context "with password too long" do
let(:password_long) { build(:user, password: Faker::Lorem.characters(number: ArchiveConfig.PASSWORD_LENGTH_MAX + 1)) }
it "does not save" do
expect(password_long.save).to be_falsey
expect(password_long.errors[:password].first).to include("is too long")
end
end
context "with existing users" do
let(:existing_user) { create(:user) }
let(:new_user) { build(:user) }
it "does not save a duplicate login" do
new_user.login = existing_user.login
expect(new_user.save).to be_falsey
expect(new_user.errors[:login].first).to include("has already been taken")
end
it "does not save a duplicate email" do
new_user.email = existing_user.email
expect(new_user.save).to be_falsey
expect(new_user.errors[:email].first).to include("This email is already associated with another account. Please try again with a different email address.")
end
it "does not save a duplicate email with different capitalization" do
new_user.email = existing_user.email.capitalize
expect(new_user.save).to be_falsey
expect(new_user.errors[:email].first).to include("This email is already associated with another account. Please try again with a different email address.")
end
end
end
end
describe "#update" do
let!(:existing_user) { create(:user) }
context "when logged in as an admin" do
before do
User.current_user = build(:admin)
end
context "when username is changed" do
before do
allow(existing_user).to receive(:justification_enabled?).and_return(false)
allow(existing_user).to receive(:ticket_number).and_return(12_345)
end
it "requires the default format" do
existing_user.update(login: "custom_username")
expect(existing_user.errors[:login].first).to eq "must use the default. Please contact your chairs to use something else."
end
it "only sets admin_renamed_at" do
freeze_time
existing_user.update!(login: "user#{existing_user.id}")
expect(existing_user.renamed_at).to be nil
expect(existing_user.admin_renamed_at).to eq(Time.current)
end
it "creates an admin log item" do
old_login = existing_user.login
existing_user.update!(login: "user#{existing_user.id}")
log_item = existing_user.reload.log_items.last
admin = User.current_user
expect(log_item.action).to eq(ArchiveConfig.ACTION_RENAME)
expect(log_item.admin_id).to eq(admin.id)
expect(log_item.note).to eq("Old Username: #{old_login}, New Username: #{existing_user.login}, Changed by: #{admin.login}, Ticket ID: #12345")
end
end
context "username was recently changed" do
before do
existing_user.update!(renamed_at: Time.current)
end
it "does not prevent changing the username" do
allow(existing_user).to receive(:justification_enabled?).and_return(false)
existing_user.update!(login: "user#{existing_user.id}")
expect(existing_user.login).to eq("user#{existing_user.id}")
end
end
end
context "when logged in as the user themselves" do
before do
User.current_user = existing_user
end
it "sets renamed_at" do
freeze_time
existing_user.update!(login: "new_username")
expect(existing_user.renamed_at).to eq(Time.current)
expect(existing_user.admin_renamed_at).to be nil
end
context "username was recently changed" do
before do
freeze_time
existing_user.update!(login: "new_login")
end
it "does not allow another rename" do
expect { existing_user.update!(login: "new") }
.to raise_error(ActiveRecord::RecordInvalid)
localized_renamed_at = I18n.l(existing_user.renamed_at)
expect(existing_user.errors[:login].first)
.to eq(
"can only be changed once every 7 days. You last changed your username on #{localized_renamed_at}."
)
end
it "allows changing email" do
existing_user.skip_reconfirmation!
existing_user.update!(email: "new_email@example.com")
expect(existing_user.email).to eq("new_email@example.com")
end
end
end
context "password was recently changed" do
before do
pw = Faker::Lorem.characters(number: ArchiveConfig.PASSWORD_LENGTH_MIN)
existing_user.update!(password: pw, password_confirmation: pw)
end
redacted_value = "[REDACTED]"
redacted_arr = Array.new(2, redacted_value)
it "audits and redacts password changes" do
last_change = existing_user.audits.pluck(:audited_changes).last
expect(last_change["encrypted_password"]).to eq(redacted_arr)
end
it "deserializes old BCrypt password changes" do
salt = SecureRandom.urlsafe_base64(15)
bcrypt_password = BCrypt::Password.create(
["another_password", salt].flatten.join,
cost: ArchiveConfig.BCRYPT_COST || 14
)
existing_user.update!(encrypted_password: bcrypt_password, password_salt: salt)
last_change = existing_user.audits.pluck(:audited_changes).last
expect(last_change["encrypted_password"]).to eq(redacted_arr)
expect(last_change["password_salt"]).to eq(redacted_arr)
end
end
context "username was changed outside window" do
before do
travel_to ArchiveConfig.USER_RENAME_LIMIT_DAYS.days.ago do
existing_user.update!(login: "new_username")
end
end
it "allows another rename" do
expect(existing_user.update!(login: "new")).to be_truthy
expect(existing_user.login).to eq("new")
end
end
context "when email is changed" do
before do
existing_user.skip_reconfirmation!
existing_user.update!(email: "newemail@example.com")
existing_user.reload
end
it "does not set renamed_at" do
expect(existing_user.renamed_at).to be_nil
end
it "creates a log item" do
log_item = existing_user.log_items.last
expect(log_item.action).to eq(ArchiveConfig.ACTION_NEW_EMAIL)
expect(log_item.admin_id).to be_nil
expect(log_item.note).to eq("System Generated")
end
end
context "as an admin" do
let(:admin) { create(:admin) }
before do
User.current_user = admin
existing_user.skip_reconfirmation!
existing_user.update!(email: "new_email@example.com")
existing_user.reload
end
it "saves an admin log item" do
log_item = existing_user.log_items.last
expect(log_item.action).to eq(ArchiveConfig.ACTION_NEW_EMAIL)
expect(log_item.admin_id).to eq(admin.id)
expect(log_item.note).to eq("Change made by #{admin.login}")
end
end
end
describe ".search_multiple_by_email" do
let(:user_bundle) { create_list(:user, 5) }
it "removes exact duplicates from the list" do
emails = user_bundle.map(&:email) << user_bundle.first.email
expect(emails.size).to be > user_bundle.size
expect(User.search_multiple_by_email(emails).first.size).to eq(emails.size - 1)
end
it "ignores case differences" do
emails = user_bundle.map(&:email) << user_bundle.first.email.upcase
expect(emails.size).to be > user_bundle.size
expect(User.search_multiple_by_email(emails).first.size).to eq(emails.size - 1)
end
it "returns found users, not found emails and the number of duplicates" do
more_emails = [user_bundle.second.email, user_bundle.first.email.upcase, "unknown@ao3.org", "UnKnown@AO3.org", "nobody@example.com"]
emails = user_bundle.map(&:email) + more_emails
found, not_found, duplicates = User.search_multiple_by_email(emails)
expect(not_found).to eq(["unknown@ao3.org", "nobody@example.com"])
expect(found.size).to eq(emails.map(&:downcase).uniq.size - not_found.size)
expect(duplicates).to eq(3)
end
end
end