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