# frozen_string_literal: true require 'spec_helper' RSpec.describe User do include ProjectForksHelper include TermsHelper include ExclusiveLeaseHelpers include LdapHelpers it_behaves_like 'having unique enum values' describe 'modules' do subject { described_class } it { is_expected.to include_module(Gitlab::ConfigHelper) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } it { is_expected.to include_module(TokenAuthenticatable) } it { is_expected.to include_module(BlocksUnsafeSerialization) } it { is_expected.to include_module(AsyncDeviseEmail) } end describe 'constants' do it { expect(described_class::COUNT_CACHE_VALIDITY_PERIOD).to be_a(Integer) } it { expect(described_class::MAX_USERNAME_LENGTH).to be_a(Integer) } it { expect(described_class::MIN_USERNAME_LENGTH).to be_a(Integer) } end describe 'delegations' do it { is_expected.to delegate_method(:path).to(:namespace).with_prefix } it { is_expected.to delegate_method(:notes_filter_for).to(:user_preference) } it { is_expected.to delegate_method(:set_notes_filter).to(:user_preference) } it { is_expected.to delegate_method(:first_day_of_week).to(:user_preference) } it { is_expected.to delegate_method(:first_day_of_week=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:timezone).to(:user_preference) } it { is_expected.to delegate_method(:timezone=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:time_display_relative).to(:user_preference) } it { is_expected.to delegate_method(:time_display_relative=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:time_format_in_24h).to(:user_preference) } it { is_expected.to delegate_method(:time_format_in_24h=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:show_whitespace_in_diffs).to(:user_preference) } it { is_expected.to delegate_method(:show_whitespace_in_diffs=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:view_diffs_file_by_file).to(:user_preference) } it { is_expected.to delegate_method(:view_diffs_file_by_file=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:tab_width).to(:user_preference) } it { is_expected.to delegate_method(:tab_width=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:sourcegraph_enabled).to(:user_preference) } it { is_expected.to delegate_method(:sourcegraph_enabled=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:gitpod_enabled).to(:user_preference) } it { is_expected.to delegate_method(:gitpod_enabled=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:setup_for_company).to(:user_preference) } it { is_expected.to delegate_method(:setup_for_company=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:render_whitespace_in_code).to(:user_preference) } it { is_expected.to delegate_method(:render_whitespace_in_code=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:markdown_surround_selection).to(:user_preference) } it { is_expected.to delegate_method(:markdown_surround_selection=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:diffs_deletion_color).to(:user_preference) } it { is_expected.to delegate_method(:diffs_deletion_color=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:diffs_addition_color).to(:user_preference) } it { is_expected.to delegate_method(:diffs_addition_color=).to(:user_preference).with_arguments(:args) } it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil } it { is_expected.to delegate_method(:pronouns).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:pronouns=).to(:user_detail).with_arguments(:args).allow_nil } it { is_expected.to delegate_method(:pronunciation).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:pronunciation=).to(:user_detail).with_arguments(:args).allow_nil } it { is_expected.to delegate_method(:bio).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:bio=).to(:user_detail).with_arguments(:args).allow_nil } it { is_expected.to delegate_method(:registration_objective).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:registration_objective=).to(:user_detail).with_arguments(:args).allow_nil } it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil } end describe 'associations' do it { is_expected.to have_one(:namespace) } it { is_expected.to have_one(:status) } it { is_expected.to have_one(:user_detail) } it { is_expected.to have_one(:atlassian_identity) } it { is_expected.to have_one(:user_highest_role) } it { is_expected.to have_one(:credit_card_validation) } it { is_expected.to have_one(:banned_user) } it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:members) } it { is_expected.to have_many(:project_members) } it { is_expected.to have_many(:group_members) } it { is_expected.to have_many(:groups) } it { is_expected.to have_many(:keys).dependent(:destroy) } it { is_expected.to have_many(:expired_today_and_unnotified_keys) } it { is_expected.to have_many(:deploy_keys).dependent(:nullify) } it { is_expected.to have_many(:group_deploy_keys) } it { is_expected.to have_many(:events).dependent(:delete_all) } it { is_expected.to have_many(:issues).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) } it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:pipelines) } it { is_expected.to have_many(:chat_names).dependent(:destroy) } it { is_expected.to have_many(:saved_replies).class_name('::Users::SavedReply') } it { is_expected.to have_many(:uploads) } it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') } it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') } it { is_expected.to have_many(:releases).dependent(:nullify) } it { is_expected.to have_many(:metrics_users_starred_dashboards).inverse_of(:user) } it { is_expected.to have_many(:reviews).inverse_of(:author) } it { is_expected.to have_many(:merge_request_assignees).inverse_of(:assignee) } it { is_expected.to have_many(:merge_request_reviewers).inverse_of(:reviewer) } it { is_expected.to have_many(:created_custom_emoji).inverse_of(:creator) } it { is_expected.to have_many(:in_product_marketing_emails) } it { is_expected.to have_many(:timelogs) } it { is_expected.to have_many(:callouts).class_name('Users::Callout') } it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') } it { is_expected.to have_many(:namespace_callouts).class_name('Users::NamespaceCallout') } it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout') } describe '#user_detail' do it 'does not persist `user_detail` by default' do expect(create(:user).user_detail).not_to be_persisted end it 'creates `user_detail` when `bio` is given' do user = create(:user, bio: 'my bio') expect(user.user_detail).to be_persisted expect(user.user_detail.bio).to eq('my bio') end it 'delegates `bio` to `user_detail`' do user = create(:user, bio: 'my bio') expect(user.bio).to eq(user.user_detail.bio) end it 'delegates `pronouns` to `user_detail`' do user = create(:user, pronouns: 'they/them') expect(user.pronouns).to eq(user.user_detail.pronouns) end it 'delegates `pronunciation` to `user_detail`' do user = create(:user, name: 'Example', pronunciation: 'uhg-zaam-pl') expect(user.pronunciation).to eq(user.user_detail.pronunciation) end it 'creates `user_detail` when `bio` is first updated' do user = create(:user) expect { user.update!(bio: 'my bio') }.to change { user.user_detail.persisted? }.from(false).to(true) end end describe '#abuse_report' do let(:current_user) { create(:user) } let(:other_user) { create(:user) } it { is_expected.to have_one(:abuse_report) } it 'refers to the abuse report whose user_id is the current user' do abuse_report = create(:abuse_report, reporter: other_user, user: current_user) expect(current_user.abuse_report).to eq(abuse_report) end it 'does not refer to the abuse report whose reporter_id is the current user' do create(:abuse_report, reporter: current_user, user: other_user) expect(current_user.abuse_report).to be_nil end it 'does not update the user_id of an abuse report when the user is updated' do abuse_report = create(:abuse_report, reporter: current_user, user: other_user) current_user.block expect(abuse_report.reload.user).to eq(other_user) end end describe '#group_members' do it 'does not include group memberships for which user is a requester' do user = create(:user) group = create(:group, :public) group.request_access(user) expect(user.group_members).to be_empty end end describe '#project_members' do it 'does not include project memberships for which user is a requester' do user = create(:user) project = create(:project, :public) project.request_access(user) expect(user.project_members).to be_empty end end end describe 'Devise emails' do let!(:user) { create(:user) } describe 'behaviour' do it 'sends emails asynchronously' do expect do user.update!(email: 'hello@hello.com') end.to have_enqueued_job.on_queue('mailers').exactly(:twice) end end context 'emails sent on changing password' do context 'when password is updated' do context 'default behaviour' do it 'enqueues the `password changed` email' do user.password = User.random_password expect { user.save! }.to have_enqueued_mail(DeviseMailer, :password_change) end it 'does not enqueue the `admin changed your password` email' do user.password = User.random_password expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin) end end context '`admin changed your password` email' do it 'is enqueued only when explicitly allowed' do user.password = User.random_password user.send_only_admin_changed_your_password_notification! expect { user.save! }.to have_enqueued_mail(DeviseMailer, :password_change_by_admin) end it '`password changed` email is not enqueued if it is explicitly allowed' do user.password = User.random_password user.send_only_admin_changed_your_password_notification! expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_changed) end it 'is not enqueued if sending notifications on password updates is turned off as per Devise config' do user.password = User.random_password user.send_only_admin_changed_your_password_notification! allow(Devise).to receive(:send_password_change_notification).and_return(false) expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin) end end end context 'when password is not updated' do it 'does not enqueue the `admin changed your password` email even if explicitly allowed' do user.name = 'John' user.send_only_admin_changed_your_password_notification! expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin) end end end end describe 'validations' do describe 'password' do let!(:user) { build_stubbed(:user) } before do allow(Devise).to receive(:password_length).and_return(8..128) allow(described_class).to receive(:password_length).and_return(10..130) end context 'length' do it { is_expected.to validate_length_of(:password).is_at_least(10).is_at_most(130) } end context 'length validator' do context 'for a short password' do before do user.password = user.password_confirmation = 'abc' end it 'does not run the default Devise password length validation' do expect(user).to be_invalid expect(user.errors.full_messages.join).not_to include('is too short (minimum is 8 characters)') end it 'runs the custom password length validator' do expect(user).to be_invalid expect(user.errors.full_messages.join).to include('is too short (minimum is 10 characters)') end end context 'for a long password' do before do user.password = user.password_confirmation = 'a' * 140 end it 'does not run the default Devise password length validation' do expect(user).to be_invalid expect(user.errors.full_messages.join).not_to include('is too long (maximum is 128 characters)') end it 'runs the custom password length validator' do expect(user).to be_invalid expect(user.errors.full_messages.join).to include('is too long (maximum is 130 characters)') end end end context 'check_password_weakness' do let(:weak_password) { "qwertyuiop" } context 'when feature flag is disabled' do before do stub_feature_flags(block_weak_passwords: false) end it 'does not add an error when password is weak' do expect(Security::WeakPasswords).not_to receive(:weak_for_user?) user.password = weak_password expect(user).to be_valid end end context 'when feature flag is enabled' do before do stub_feature_flags(block_weak_passwords: true) end it 'checks for password weakness when password changes' do expect(Security::WeakPasswords).to receive(:weak_for_user?) .with(weak_password, user).and_call_original user.password = weak_password expect(user).not_to be_valid end it 'adds an error when password is weak' do user.password = weak_password expect(user).not_to be_valid expect(user.errors).to be_of_kind(:password, 'must not contain commonly used combinations of words and letters') end it 'is valid when password is not weak' do user.password = ::User.random_password expect(user).to be_valid end it 'is valid when weak password was already set' do user = build(:user, password: weak_password) user.save!(validate: false) expect(Security::WeakPasswords).not_to receive(:weak_for_user?) # Change an unrelated value user.name = "Example McExampleFace" expect(user).to be_valid end end end end describe 'name' do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_length_of(:name).is_at_most(255) } end describe 'first name' do it { is_expected.to validate_length_of(:first_name).is_at_most(127) } end describe 'last name' do it { is_expected.to validate_length_of(:last_name).is_at_most(127) } end describe 'preferred_language' do context 'when its value is nil in the database' do let(:user) { build(:user, preferred_language: nil) } it 'falls back to I18n.default_locale when empty in the database' do expect(user.preferred_language).to eq I18n.default_locale.to_s end it 'falls back to english when I18n.default_locale is not an available language' do I18n.default_locale = :kl default_preferred_language = user.send(:default_preferred_language) expect(user.preferred_language).to eq default_preferred_language end end end describe 'username' do it 'validates presence' do expect(subject).to validate_presence_of(:username) end it 'rejects denied names' do user = build(:user, username: 'dashboard') expect(user).not_to be_valid expect(user.errors.messages[:username]).to eq ['dashboard is a reserved name'] end it 'allows child names' do user = build(:user, username: 'avatar') expect(user).to be_valid end it 'allows wildcard names' do user = build(:user, username: 'blob') expect(user).to be_valid end context 'when username is changed' do let(:user) { build_stubbed(:user, username: 'old_path', namespace: build_stubbed(:user_namespace)) } it 'validates move_dir is allowed for the namespace' do expect(user.namespace).to receive(:any_project_has_container_registry_tags?).and_return(true) user.username = 'new_path' expect(user).to be_invalid expect(user.errors.messages[:username].first).to eq(_('cannot be changed if a personal project has container registry tags.')) end end context 'when the username is in use by another user' do let(:username) { 'foo' } let!(:other_user) { create(:user, username: username) } it 'is invalid' do user = build(:user, username: username) expect(user).not_to be_valid expect(user.errors.full_messages).to eq(['Username has already been taken']) end end it 'validates format' do Mime::EXTENSION_LOOKUP.keys.each do |type| user = build(:user, username: "test.#{type}") expect(user).not_to be_valid expect(user.errors.full_messages).to include('Username ending with a reserved file extension is not allowed.') expect(build(:user, username: "test#{type}")).to be_valid end end it 'validates format on updated record' do expect(create(:user).update(username: 'profile.html')).to be_falsey end end it 'has a DB-level NOT NULL constraint on projects_limit' do user = create(:user) expect(user.persisted?).to eq(true) expect do user.update_columns(projects_limit: nil) end.to raise_error(ActiveRecord::StatementInvalid) end it { is_expected.to validate_presence_of(:projects_limit) } it { is_expected.to validate_numericality_of(:projects_limit) } it { is_expected.to allow_value(0).for(:projects_limit) } it { is_expected.not_to allow_value(-1).for(:projects_limit) } it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) } it_behaves_like 'an object with email-formatted attributes', :email do subject { build(:user) } end it_behaves_like 'an object with email-formatted attributes', :public_email, :notification_email do subject { create(:user).tap { |user| user.emails << build(:email, email: email_value, confirmed_at: Time.current) } } end describe '#commit_email_or_default' do subject(:user) { create(:user) } it 'defaults to the primary email' do expect(user.email).to be_present expect(user.commit_email_or_default).to eq(user.email) end it 'defaults to the primary email when the column in the database is null' do user.update_column(:commit_email, nil) found_user = described_class.find_by(id: user.id) expect(found_user.commit_email_or_default).to eq(user.email) end it 'returns the private commit email when commit_email has _private' do user.update_column(:commit_email, Gitlab::PrivateCommitEmail::TOKEN) expect(user.commit_email_or_default).to eq(user.private_commit_email) end end describe '#commit_email=' do subject(:user) { create(:user) } it 'can be set to a confirmed email' do confirmed = create(:email, :confirmed, user: user) user.commit_email = confirmed.email expect(user).to be_valid end it 'can not be set to an unconfirmed email' do unconfirmed = create(:email, user: user) user.commit_email = unconfirmed.email expect(user).not_to be_valid end it 'can not be set to a non-existent email' do user.commit_email = 'non-existent-email@nonexistent.nonexistent' expect(user).not_to be_valid end it 'can not be set to an invalid email, even if confirmed' do confirmed = create(:email, :confirmed, :skip_validate, user: user, email: 'invalid') user.commit_email = confirmed.email expect(user).not_to be_valid end end describe 'email' do let(:expected_error) { _('is not allowed for sign-up. Please use your regular email address. Check with your administrator.') } context 'when no signup domains allowed' do before do stub_application_setting(domain_allowlist: []) end it 'accepts any email' do user = build(:user, email: "info@example.com") expect(user).to be_valid end end context 'bad regex' do before do stub_application_setting(domain_allowlist: ['([a-zA-Z0-9]+)+\.com']) end it 'does not hang on evil input' do user = build(:user, email: 'user@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!.com') expect do Timeout.timeout(2.seconds) { user.valid? } end.not_to raise_error end end context 'when a signup domain is allowed and subdomains are allowed' do before do stub_application_setting(domain_allowlist: ['example.com', '*.example.com']) end it 'accepts info@example.com' do user = build(:user, email: "info@example.com") expect(user).to be_valid end it 'accepts info@test.example.com' do user = build(:user, email: "info@test.example.com") expect(user).to be_valid end it 'rejects example@test.com' do user = build(:user, email: "example@test.com") expect(user).to be_invalid expect(user.errors.messages[:email].first).to eq(expected_error) end it 'does not allow user to update email to a non-allowlisted domain' do user = create(:user, email: "info@test.example.com") expect { user.update!(email: "test@notexample.com") } .to raise_error(StandardError, 'Validation failed: Email is not allowed. Please use your regular email address. Check with your administrator.') end end context 'when a signup domain is allowed and subdomains are not allowed' do before do stub_application_setting(domain_allowlist: ['example.com']) end it 'accepts info@example.com' do user = build(:user, email: "info@example.com") expect(user).to be_valid end it 'rejects info@test.example.com' do user = build(:user, email: "info@test.example.com") expect(user).to be_invalid expect(user.errors.messages[:email].first).to eq(expected_error) end it 'rejects example@test.com' do user = build(:user, email: "example@test.com") expect(user).to be_invalid expect(user.errors.messages[:email].first).to eq(expected_error) end it 'accepts example@test.com when added by another user' do user = build(:user, email: "example@test.com", created_by_id: 1) expect(user).to be_valid end end context 'domain denylist' do before do stub_application_setting(domain_denylist_enabled: true) stub_application_setting(domain_denylist: ['example.com']) end context 'bad regex' do before do stub_application_setting(domain_denylist: ['([a-zA-Z0-9]+)+\.com']) end it 'does not hang on evil input' do user = build(:user, email: 'user@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!.com') expect do Timeout.timeout(2.seconds) { user.valid? } end.not_to raise_error end end context 'when a signup domain is denied' do it 'accepts info@test.com' do user = build(:user, email: 'info@test.com') expect(user).to be_valid end it 'rejects info@example.com' do user = build(:user, email: 'info@example.com') expect(user).not_to be_valid expect(user.errors.messages[:email].first).to eq(expected_error) end it 'accepts info@example.com when added by another user' do user = build(:user, email: 'info@example.com', created_by_id: 1) expect(user).to be_valid end it 'does not allow user to update email to a denied domain' do user = create(:user, email: 'info@test.com') expect { user.update!(email: 'info@example.com') } .to raise_error(StandardError, 'Validation failed: Email is not allowed. Please use your regular email address. Check with your administrator.') end end context 'when a signup domain is denied but a wildcard subdomain is allowed' do before do stub_application_setting(domain_denylist: ['test.example.com']) stub_application_setting(domain_allowlist: ['*.example.com']) end it 'gives priority to allowlist and allow info@test.example.com' do user = build(:user, email: 'info@test.example.com') expect(user).to be_valid end end context 'with both lists containing a domain' do before do stub_application_setting(domain_allowlist: ['test.com']) end it 'accepts info@test.com' do user = build(:user, email: 'info@test.com') expect(user).to be_valid end it 'rejects info@example.com' do user = build(:user, email: 'info@example.com') expect(user).not_to be_valid expect(user.errors.messages[:email].first).to eq(expected_error) end end end context 'email restrictions' do context 'when email restriction is disabled' do before do stub_application_setting(email_restrictions_enabled: false) stub_application_setting(email_restrictions: '\+') end it 'does accept email address' do user = build(:user, email: 'info+1@test.com') expect(user).to be_valid end end context 'when email restrictions is enabled' do before do stub_application_setting(email_restrictions_enabled: true) stub_application_setting(email_restrictions: '([\+]|\b(\w*gitlab.com\w*)\b)') end it 'does not accept email address with + characters' do user = build(:user, email: 'info+1@test.com') expect(user).not_to be_valid end it 'does not accept email with a gitlab domain' do user = build(:user, email: 'info@gitlab.com') expect(user).not_to be_valid end it 'adds an error message when email is not accepted' do user = build(:user, email: 'info@gitlab.com') expect(user).not_to be_valid expect(user.errors.messages[:email].first).to eq(expected_error) end it 'does not allow user to update email to a restricted domain' do user = create(:user, email: 'info@test.com') expect { user.update!(email: 'info@gitlab.com') } .to raise_error(StandardError, 'Validation failed: Email is not allowed. Please use your regular email address. Check with your administrator.') end it 'does accept a valid email address' do user = build(:user, email: 'info@test.com') expect(user).to be_valid end context 'when created_by_id is set' do it 'does accept the email address' do user = build(:user, email: 'info+1@test.com', created_by_id: 1) expect(user).to be_valid end end end end context 'when secondary email is same as primary' do let(:user) { create(:user, email: 'user@example.com') } it 'lets user change primary email without failing validations' do user.commit_email = user.email user.notification_email = user.email user.public_email = user.email user.save! user.email = 'newemail@example.com' user.confirm expect(user).to be_valid end end context 'when commit_email is changed to _private' do it 'passes user validations' do user = create(:user) user.commit_email = '_private' expect(user).to be_valid end end end end describe 'scopes' do context 'blocked users' do let_it_be(:active_user) { create(:user) } let_it_be(:blocked_user) { create(:user, :blocked) } let_it_be(:ldap_blocked_user) { create(:omniauth_user, :ldap_blocked) } let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval) } let_it_be(:banned_user) { create(:user, :banned) } describe '.blocked' do subject { described_class.blocked } it 'returns only blocked users' do expect(subject).to include( blocked_user, ldap_blocked_user ) expect(subject).not_to include(active_user, blocked_pending_approval_user, banned_user) end end describe '.blocked_pending_approval' do subject { described_class.blocked_pending_approval } it 'returns only pending approval users' do expect(subject).to contain_exactly(blocked_pending_approval_user) end end describe '.banned' do subject { described_class.banned } it 'returns only banned users' do expect(subject).to contain_exactly(banned_user) end end end describe '.with_two_factor' do it 'returns users with 2fa enabled via OTP' do user_with_2fa = create(:user, :two_factor_via_otp) user_without_2fa = create(:user) users_with_two_factor = described_class.with_two_factor.pluck(:id) expect(users_with_two_factor).to include(user_with_2fa.id) expect(users_with_two_factor).not_to include(user_without_2fa.id) end shared_examples 'returns the right users' do |trait| it 'returns users with 2fa enabled via hardware token' do user_with_2fa = create(:user, trait) user_without_2fa = create(:user) users_with_two_factor = described_class.with_two_factor.pluck(:id) expect(users_with_two_factor).to include(user_with_2fa.id) expect(users_with_two_factor).not_to include(user_without_2fa.id) end it 'returns users with 2fa enabled via OTP and hardware token' do user_with_2fa = create(:user, :two_factor_via_otp, trait) user_without_2fa = create(:user) users_with_two_factor = described_class.with_two_factor.pluck(:id) expect(users_with_two_factor).to eq([user_with_2fa.id]) expect(users_with_two_factor).not_to include(user_without_2fa.id) end it 'works with ORDER BY' do user_with_2fa = create(:user, :two_factor_via_otp, trait) expect(described_class .with_two_factor .reorder_by_name).to eq([user_with_2fa]) end end describe 'and U2F' do it_behaves_like "returns the right users", :two_factor_via_u2f end describe 'and WebAuthn' do it_behaves_like "returns the right users", :two_factor_via_webauthn end end describe '.without_two_factor' do it 'excludes users with 2fa enabled via OTP' do user_with_2fa = create(:user, :two_factor_via_otp) user_without_2fa = create(:user) users_without_two_factor = described_class.without_two_factor.pluck(:id) expect(users_without_two_factor).to include(user_without_2fa.id) expect(users_without_two_factor).not_to include(user_with_2fa.id) end describe 'and u2f' do it 'excludes users with 2fa enabled via U2F' do user_with_2fa = create(:user, :two_factor_via_u2f) user_without_2fa = create(:user) users_without_two_factor = described_class.without_two_factor.pluck(:id) expect(users_without_two_factor).to include(user_without_2fa.id) expect(users_without_two_factor).not_to include(user_with_2fa.id) end it 'excludes users with 2fa enabled via OTP and U2F' do user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) user_without_2fa = create(:user) users_without_two_factor = described_class.without_two_factor.pluck(:id) expect(users_without_two_factor).to include(user_without_2fa.id) expect(users_without_two_factor).not_to include(user_with_2fa.id) end end describe 'and webauthn' do it 'excludes users with 2fa enabled via WebAuthn' do user_with_2fa = create(:user, :two_factor_via_webauthn) user_without_2fa = create(:user) users_without_two_factor = described_class.without_two_factor.pluck(:id) expect(users_without_two_factor).to include(user_without_2fa.id) expect(users_without_two_factor).not_to include(user_with_2fa.id) end it 'excludes users with 2fa enabled via OTP and WebAuthn' do user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_webauthn) user_without_2fa = create(:user) users_without_two_factor = described_class.without_two_factor.pluck(:id) expect(users_without_two_factor).to include(user_without_2fa.id) expect(users_without_two_factor).not_to include(user_with_2fa.id) end end end describe '.random_password' do let(:random_password) { described_class.random_password } before do expect(User).to receive(:password_length).and_return(88..128) end context 'length' do it 'conforms to the current password length settings' do expect(random_password.length).to eq(128) end end end describe '.password_length' do let(:password_length) { described_class.password_length } it 'is expected to be a Range' do expect(password_length).to be_a(Range) end context 'minimum value' do before do stub_application_setting(minimum_password_length: 101) end it 'is determined by the current value of `minimum_password_length` attribute of application_setting' do expect(password_length.min).to eq(101) end end context 'maximum value' do it 'is determined by the current value of `Devise.password_length.max`' do expect(password_length.max).to eq(Devise.password_length.max) end end end describe '.limit_to_todo_authors' do context 'when filtering by todo authors' do let(:user1) { create(:user) } let(:user2) { create(:user) } before do create(:todo, user: user1, author: user1, state: :done) create(:todo, user: user2, author: user2, state: :pending) end it 'only returns users that have authored todos' do users = described_class.limit_to_todo_authors( user: user2, with_todos: true, todo_state: :pending ) expect(users).to eq([user2]) end it 'ignores users that do not have a todo in the matching state' do users = described_class.limit_to_todo_authors( user: user1, with_todos: true, todo_state: :pending ) expect(users).to be_empty end end context 'when not filtering by todo authors' do it 'returns the input relation' do user1 = create(:user) user2 = create(:user) rel = described_class.limit_to_todo_authors(user: user1) expect(rel).to include(user1, user2) end end context 'when no user is provided' do it 'returns the input relation' do user1 = create(:user) user2 = create(:user) rel = described_class.limit_to_todo_authors expect(rel).to include(user1, user2) end end end describe '.by_username' do it 'finds users regardless of the case passed' do user = create(:user, username: 'CaMeLcAsEd') user2 = create(:user, username: 'UPPERCASE') expect(described_class.by_username(%w(CAMELCASED uppercase))) .to contain_exactly(user, user2) end it 'finds a single user regardless of the case passed' do user = create(:user, username: 'CaMeLcAsEd') expect(described_class.by_username('CAMELCASED')) .to contain_exactly(user) end end describe '.with_expiring_and_not_notified_personal_access_tokens' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } let_it_be(:user3) { create(:user) } let_it_be(:expired_token) { create(:personal_access_token, user: user1, expires_at: 2.days.ago) } let_it_be(:revoked_token) { create(:personal_access_token, user: user1, revoked: true) } let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user1, expires_at: 2.days.from_now) } let_it_be(:valid_token_and_notified) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now, expire_notification_delivered: true) } let_it_be(:valid_token1) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) } let_it_be(:valid_token2) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) } let(:users) { described_class.with_expiring_and_not_notified_personal_access_tokens(from) } context 'in one day' do let(:from) { 1.day.from_now } it "doesn't include an user" do expect(users).to be_empty end end context 'in three days' do let(:from) { 3.days.from_now } it 'only includes user2' do expect(users).to contain_exactly(user2) end end end describe '.with_personal_access_tokens_expired_today' do let_it_be(:user1) { create(:user) } let_it_be(:expired_today) { create(:personal_access_token, user: user1, expires_at: Date.current) } let_it_be(:user2) { create(:user) } let_it_be(:revoked_token) { create(:personal_access_token, user: user2, expires_at: Date.current, revoked: true) } let_it_be(:user3) { create(:user) } let_it_be(:impersonated_token) { create(:personal_access_token, user: user3, expires_at: Date.current, impersonation: true) } let_it_be(:user4) { create(:user) } let_it_be(:already_notified) { create(:personal_access_token, user: user4, expires_at: Date.current, after_expiry_notification_delivered: true) } it 'returns users whose token has expired today' do expect(described_class.with_personal_access_tokens_expired_today).to contain_exactly(user1) end end context 'SSH key expiration scopes' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } let_it_be(:expired_today_not_notified) { create(:key, :expired_today, user: user1) } let_it_be(:expired_today_already_notified) { create(:key, :expired_today, user: user2, expiry_notification_delivered_at: Time.current) } let_it_be(:expiring_soon_not_notified) { create(:key, expires_at: 2.days.from_now, user: user2) } let_it_be(:expiring_soon_notified) { create(:key, expires_at: 2.days.from_now, user: user1, before_expiry_notification_delivered_at: Time.current) } describe '.with_ssh_key_expiring_soon' do it 'returns users whose keys will expire soon' do expect(described_class.with_ssh_key_expiring_soon).to contain_exactly(user2) end end end describe '.active_without_ghosts' do let_it_be(:user1) { create(:user, :external) } let_it_be(:user2) { create(:user, state: 'blocked') } let_it_be(:user3) { create(:user, :ghost) } let_it_be(:user4) { create(:user) } it 'returns all active users but ghost users' do expect(described_class.active_without_ghosts).to match_array([user1, user4]) end end describe '.without_ghosts' do let_it_be(:user1) { create(:user, :external) } let_it_be(:user2) { create(:user, state: 'blocked') } let_it_be(:user3) { create(:user, :ghost) } it 'returns users without ghosts users' do expect(described_class.without_ghosts).to match_array([user1, user2]) end end describe '.for_todos' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } let_it_be(:issue) { create(:issue) } let_it_be(:todo1) { create(:todo, target: issue, author: user1, user: user1) } let_it_be(:todo2) { create(:todo, target: issue, author: user1, user: user1) } let_it_be(:todo3) { create(:todo, target: issue, author: user2, user: user2) } it 'returns users for the given todos' do expect(described_class.for_todos(issue.todos)) .to contain_exactly(user1, user2) end end describe '.order_recent_last_activity' do it 'sorts users by activity and id to make the ordes deterministic' do expect(described_class.order_recent_last_activity.to_sql).to include( 'ORDER BY "users"."last_activity_on" DESC NULLS LAST, "users"."id" ASC') end end describe '.order_oldest_last_activity' do it 'sorts users by activity and id to make the ordes deterministic' do expect(described_class.order_oldest_last_activity.to_sql).to include( 'ORDER BY "users"."last_activity_on" ASC NULLS FIRST, "users"."id" DESC') end end end context 'strip attributes' do context 'name' do let(:user) { User.new(name: ' John Smith ') } it 'strips whitespaces on validation' do expect { user.valid? }.to change { user.name }.to('John Smith') end end end describe 'Respond to' do it { is_expected.to respond_to(:admin?) } it { is_expected.to respond_to(:name) } it { is_expected.to respond_to(:external?) } end describe 'before save hook' do describe '#default_private_profile_to_false' do let(:user) { create(:user, private_profile: true) } it 'converts nil to false' do user.private_profile = nil user.save! expect(user.private_profile).to eq false end end context 'when saving an external user' do let(:user) { create(:user) } let(:external_user) { create(:user, external: true) } it 'sets other properties as well' do expect(external_user.can_create_team).to be_falsey expect(external_user.can_create_group).to be_falsey expect(external_user.projects_limit).to be 0 end end describe '#check_for_verified_email' do let(:user) { create(:user) } let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) } it 'allows a verified secondary email to be used as the primary without needing reconfirmation' do user.update!(email: secondary.email) user.reload expect(user.email).to eq secondary.email expect(user.unconfirmed_email).to eq nil expect(user.confirmed?).to be_truthy end end end describe 'after commit hook' do describe 'when the primary email is updated' do before do @user = create(:user, email: 'primary@example.com').tap do |user| user.skip_reconfirmation! end @secondary = create :email, email: 'secondary@example.com', user: @user @user.reload end it 'keeps old primary to secondary emails when secondary is a new email' do @user.update!(email: 'new_primary@example.com') @user.reload expect(@user.emails.count).to eq 3 expect(@user.emails.pluck(:email)).to match_array([@secondary.email, 'primary@example.com', 'new_primary@example.com']) end context 'when the first email was unconfirmed and the second email gets confirmed' do let(:user) { create(:user, :unconfirmed, email: 'should-be-unconfirmed@test.com') } before do user.update!(email: 'should-be-confirmed@test.com') user.confirm end it 'updates user.email' do expect(user.email).to eq('should-be-confirmed@test.com') end it 'confirms user.email' do expect(user).to be_confirmed end it 'does not add unconfirmed email to secondary' do expect(user.emails.map(&:email)).not_to include('should-be-unconfirmed@test.com') end it 'has only one email association' do expect(user.emails.size).to eq(1) end end end context 'when an existing email record is set as primary' do let(:user) { create(:user, email: 'confirmed@test.com') } context 'when it is unconfirmed' do let(:originally_unconfirmed_email) { 'should-stay-unconfirmed@test.com' } before do user.emails << create(:email, email: originally_unconfirmed_email, confirmed_at: nil) user.update!(email: originally_unconfirmed_email) end it 'keeps the user confirmed' do expect(user).to be_confirmed end it 'keeps the original email' do expect(user.email).to eq('confirmed@test.com') end context 'when the email gets confirmed' do before do user.confirm end it 'keeps the user confirmed' do expect(user).to be_confirmed end it 'updates the email' do expect(user.email).to eq(originally_unconfirmed_email) end end end context 'when it is confirmed' do let!(:old_confirmed_email) { user.email } let(:confirmed_email) { 'already-confirmed@test.com' } before do user.emails << create(:email, :confirmed, email: confirmed_email) user.update!(email: confirmed_email) end it 'keeps the user confirmed' do expect(user).to be_confirmed end it 'updates the email' do expect(user.email).to eq(confirmed_email) end it 'keeps the old email' do email = user.reload.emails.first expect(email.email).to eq(old_confirmed_email) expect(email).to be_confirmed end end end context 'when unconfirmed user deletes a confirmed additional email' do let(:user) { create(:user, :unconfirmed) } before do user.emails << create(:email, :confirmed) end it 'does not affect the confirmed status' do expect { user.emails.confirmed.destroy_all }.not_to change { user.confirmed? } # rubocop: disable Cop/DestroyAll end end describe 'when changing email' do let(:user) { create(:user) } let(:new_email) { 'new-email@example.com' } context 'if notification_email was nil' do it 'sets :unconfirmed_email' do expect do user.tap { |u| u.update!(email: new_email) }.reload end.to change(user, :unconfirmed_email).to(new_email) end it 'does not change notification_email or notification_email_or_default before email is confirmed' do expect do user.tap { |u| u.update!(email: new_email) }.reload end.not_to change(user, :notification_email_or_default) expect(user.notification_email).to be_nil end it 'updates notification_email_or_default to the new email once confirmed' do user.update!(email: new_email) expect do user.tap(&:confirm).reload end.to change(user, :notification_email_or_default).to eq(new_email) expect(user.notification_email).to be_nil end end context 'when notification_email is set to a secondary email' do let!(:email_attrs) { attributes_for(:email, :confirmed, user: user) } let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) } before do user.emails.create!(email_attrs) user.tap { |u| u.update!(notification_email: email_attrs[:email]) }.reload end it 'does not change notification_email to email before email is confirmed' do expect do user.tap { |u| u.update!(email: new_email) }.reload end.not_to change(user, :notification_email) end it 'does not change notification_email to email once confirmed' do user.update!(email: new_email) expect do user.tap(&:confirm).reload end.not_to change(user, :notification_email) end end end describe '#update_invalid_gpg_signatures' do let(:user) do create(:user, email: 'tula.torphy@abshire.ca').tap do |user| user.skip_reconfirmation! end end it 'does nothing when the name is updated' do expect(user).not_to receive(:update_invalid_gpg_signatures) user.update!(name: 'Bette') end it 'synchronizes the gpg keys when the email is updated' do expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice) user.update!(email: 'shawnee.ritchie@denesik.com') end end end describe 'name getters' do let(:user) { create(:user, name: 'Kane Martin William') } it 'derives first name from full name, if not present' do expect(user.first_name).to eq('Kane') end it 'derives last name from full name, if not present' do expect(user.last_name).to eq('Martin William') end end describe '#highest_role' do let_it_be(:user) { create(:user) } context 'when user_highest_role does not exist' do it 'returns NO_ACCESS' do expect(user.highest_role).to eq(Gitlab::Access::NO_ACCESS) end end context 'when user_highest_role exists' do context 'stored highest access level is nil' do it 'returns Gitlab::Access::NO_ACCESS' do create(:user_highest_role, user: user) expect(user.highest_role).to eq(Gitlab::Access::NO_ACCESS) end end context 'stored highest access level present' do context 'with association :user_highest_role' do let(:another_user) { create(:user) } before do create(:user_highest_role, :maintainer, user: user) create(:user_highest_role, :developer, user: another_user) end it 'returns the correct highest role' do users = User.includes(:user_highest_role).where(id: [user.id, another_user.id]) expect(users.collect { |u| [u.id, u.highest_role] }).to contain_exactly( [user.id, Gitlab::Access::MAINTAINER], [another_user.id, Gitlab::Access::DEVELOPER] ) end end end end end describe '#credit_card_validated_at' do let_it_be(:user) { create(:user) } context 'when credit_card_validation does not exist' do it 'returns nil' do expect(user.credit_card_validated_at).to be nil end end context 'when credit_card_validation exists' do it 'returns the credit card validated time' do credit_card_validated_time = Time.current - 1.day create(:credit_card_validation, credit_card_validated_at: credit_card_validated_time, user: user) expect(user.credit_card_validated_at).to eq(credit_card_validated_time) end end end describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do let(:request) { double('request', remote_ip: "127.0.0.1") } let(:user) { create(:user) } it 'writes trackable attributes' do expect do user.update_tracked_fields!(request) end.to change { user.reload.current_sign_in_at } end it 'does not write trackable attributes when called a second time within the hour' do user.update_tracked_fields!(request) expect do user.update_tracked_fields!(request) end.not_to change { user.reload.current_sign_in_at } end it 'writes trackable attributes for a different user' do user2 = create(:user) user.update_tracked_fields!(request) expect do user2.update_tracked_fields!(request) end.to change { user2.reload.current_sign_in_at } end it 'does not write if the DB is in read-only mode' do expect(Gitlab::Database).to receive(:read_only?).and_return(true) expect do user.update_tracked_fields!(request) end.not_to change { user.reload.current_sign_in_at } end end shared_context 'user keys' do let(:user) { create(:user) } let!(:key) { create(:key, user: user) } let!(:deploy_key) { create(:deploy_key, user: user) } end describe '#keys' do include_context 'user keys' context 'with key and deploy key stored' do it 'returns stored key, but not deploy_key' do expect(user.keys).to include key expect(user.keys).not_to include deploy_key end end end describe '#accessible_deploy_keys' do let(:user) { create(:user) } let(:project) { create(:project) } let!(:private_deploy_keys_project) { create(:deploy_keys_project) } let!(:public_deploy_keys_project) { create(:deploy_keys_project) } let!(:accessible_deploy_keys_project) { create(:deploy_keys_project, project: project) } before do public_deploy_keys_project.deploy_key.update!(public: true) project.add_developer(user) end it 'user can only see deploy keys accessible to right projects' do expect(user.accessible_deploy_keys).to match_array([public_deploy_keys_project.deploy_key, accessible_deploy_keys_project.deploy_key]) end end describe '#deploy_keys' do include_context 'user keys' context 'with key and deploy key stored' do it 'returns stored deploy key, but not normal key' do expect(user.deploy_keys).to include deploy_key expect(user.deploy_keys).not_to include key end end end describe '#confirm' do let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days } let(:extant_confirmation_sent_at) { Date.today } before do allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) end let(:user) do create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com').tap do |user| user.update!(confirmation_sent_at: confirmation_sent_at) end end shared_examples_for 'unconfirmed user' do it 'returns unconfirmed' do expect(user.confirmed?).to be_falsey end end context 'when the confirmation period has expired' do let(:confirmation_sent_at) { expired_confirmation_sent_at } it_behaves_like 'unconfirmed user' it 'does not confirm the user' do user.confirm expect(user.confirmed?).to be_falsey end it 'does not add the confirmed primary email to emails' do user.confirm expect(user.emails.confirmed.map(&:email)).not_to include(user.email) end end context 'when the confirmation period has not expired' do let(:confirmation_sent_at) { extant_confirmation_sent_at } it_behaves_like 'unconfirmed user' it 'confirms a user' do user.confirm expect(user.confirmed?).to be_truthy end it 'adds the confirmed primary email to emails' do expect(user.emails.confirmed.map(&:email)).not_to include(user.unconfirmed_email) user.confirm expect(user.emails.confirmed.map(&:email)).to include(user.email) end context 'when the primary email is already included in user.emails' do let(:expired_confirmation_sent_at_for_email) { Date.today - Email.confirm_within - 7.days } let(:extant_confirmation_sent_at_for_email) { Date.today } let!(:email) do create(:email, email: user.unconfirmed_email, user: user).tap do |email| email.update!(confirmation_sent_at: confirmation_sent_at_for_email) end end context 'when the confirmation period of the email record has expired' do let(:confirmation_sent_at_for_email) { expired_confirmation_sent_at_for_email } it 'does not confirm the email record' do user.confirm expect(email.reload.confirmed?).to be_falsey end end context 'when the confirmation period of the email record has not expired' do let(:confirmation_sent_at_for_email) { extant_confirmation_sent_at_for_email } it 'confirms the email record' do user.confirm expect(email.reload.confirmed?).to be_truthy end end end end end describe 'saving primary email to the emails table' do context 'when calling skip_reconfirmation! while updating the primary email' do let(:user) { create(:user, email: 'primary@example.com') } it 'adds the new email to emails' do user.skip_reconfirmation! user.update!(email: 'new_primary@example.com') expect(user.email).to eq('new_primary@example.com') expect(user.unconfirmed_email).to be_nil expect(user).to be_confirmed expect(user.emails.pluck(:email)).to include('new_primary@example.com') expect(user.emails.find_by(email: 'new_primary@example.com')).to be_confirmed end end context 'when the email is changed but not confirmed' do let(:user) { create(:user, email: 'primary@example.com') } before do user.update!(email: 'new_primary@example.com') end it 'does not add the new email to emails yet' do expect(user.unconfirmed_email).to eq('new_primary@example.com') expect(user.email).to eq('primary@example.com') expect(user).to be_confirmed expect(user.emails.pluck(:email)).not_to include('new_primary@example.com') end it 'adds the new email to emails upon confirmation' do user.confirm expect(user.email).to eq('new_primary@example.com') expect(user).to be_confirmed expect(user.emails.pluck(:email)).to include('new_primary@example.com') end end context 'when the user is created as not confirmed' do let(:user) { create(:user, :unconfirmed, email: 'primary@example.com') } it 'does not add the email to emails yet' do expect(user).not_to be_confirmed expect(user.emails.pluck(:email)).not_to include('primary@example.com') end it 'adds the email to emails upon confirmation' do user.confirm expect(user.emails.pluck(:email)).to include('primary@example.com') end end context 'when the user is created as confirmed' do let(:user) { create(:user, email: 'primary@example.com', confirmed_at: DateTime.now.utc) } it 'adds the email to emails' do expect(user).to be_confirmed expect(user.emails.pluck(:email)).to include('primary@example.com') end end context 'when skip_confirmation! is called' do let(:user) { build(:user, :unconfirmed, email: 'primary@example.com') } it 'adds the email to emails' do user.skip_confirmation! user.save! expect(user).to be_confirmed expect(user.emails.pluck(:email)).to include('primary@example.com') end end end describe '#force_confirm' do let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days } let(:extant_confirmation_sent_at) { Date.today } let(:user) do create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com').tap do |user| user.update!(confirmation_sent_at: confirmation_sent_at) end end shared_examples_for 'unconfirmed user' do it 'returns unconfirmed' do expect(user.confirmed?).to be_falsey end end shared_examples_for 'confirms the user on force_confirm' do it 'confirms a user' do user.force_confirm expect(user.confirmed?).to be_truthy end end shared_examples_for 'adds the confirmed primary email to emails' do it 'adds the confirmed primary email to emails' do expect(user.emails.confirmed.map(&:email)).not_to include(user.email) user.force_confirm expect(user.emails.confirmed.map(&:email)).to include(user.email) end end shared_examples_for 'confirms the email record if the primary email was already present in user.emails' do context 'when the primary email is already included in user.emails' do let(:expired_confirmation_sent_at_for_email) { Date.today - Email.confirm_within - 7.days } let(:extant_confirmation_sent_at_for_email) { Date.today } let!(:email) do create(:email, email: user.unconfirmed_email, user: user).tap do |email| email.update!(confirmation_sent_at: confirmation_sent_at_for_email) end end shared_examples_for 'confirms the email record' do it 'confirms the email record' do user.force_confirm expect(email.reload.confirmed?).to be_truthy end end context 'when the confirmation period of the email record has expired' do let(:confirmation_sent_at_for_email) { expired_confirmation_sent_at_for_email } it_behaves_like 'confirms the email record' end context 'when the confirmation period of the email record has not expired' do let(:confirmation_sent_at_for_email) { extant_confirmation_sent_at_for_email } it_behaves_like 'confirms the email record' end end end context 'when the confirmation period has expired' do let(:confirmation_sent_at) { expired_confirmation_sent_at } it_behaves_like 'unconfirmed user' it_behaves_like 'confirms the user on force_confirm' it_behaves_like 'adds the confirmed primary email to emails' it_behaves_like 'confirms the email record if the primary email was already present in user.emails' end context 'when the confirmation period has not expired' do let(:confirmation_sent_at) { extant_confirmation_sent_at } it_behaves_like 'unconfirmed user' it_behaves_like 'confirms the user on force_confirm' it_behaves_like 'adds the confirmed primary email to emails' it_behaves_like 'confirms the email record if the primary email was already present in user.emails' end end context 'if the user is created with confirmed_at set to a time' do let!(:user) { create(:user, email: 'test@gitlab.com', confirmed_at: Time.now.utc) } it 'adds the confirmed primary email to emails upon creation' do expect(user.emails.confirmed.map(&:email)).to include(user.email) end end describe '#to_reference' do let(:user) { create(:user) } it 'returns a String reference to the object' do expect(user.to_reference).to eq "@#{user.username}" end end describe '#generate_password' do it 'does not generate password by default' do password = User.random_password user = create(:user, password: password) expect(user.password).to eq(password) end end describe 'ensure user preference' do it 'has user preference upon user initialization' do user = build(:user) expect(user.user_preference).to be_present expect(user.user_preference).not_to be_persisted end end describe 'ensure incoming email token' do it 'has incoming email token' do user = create(:user) expect(user.incoming_email_token).not_to be_blank end it 'uses SecureRandom to generate the incoming email token' do allow_next_instance_of(User) do |user| allow(user).to receive(:update_highest_role) end allow_next_instance_of(Namespaces::UserNamespace) do |namespace| allow(namespace).to receive(:schedule_sync_event_worker) end expect(SecureRandom).to receive(:hex).with(no_args).and_return('3b8ca303') user = create(:user) expect(user.incoming_email_token).to eql('gitlab') end end describe '#ensure_user_rights_and_limits' do describe 'with external user' do let(:user) { create(:user, external: true) } it 'receives callback when external changes' do expect(user).to receive(:ensure_user_rights_and_limits) user.update!(external: false) end it 'ensures correct rights and limits for user' do stub_config_setting(default_can_create_group: true) expect { user.update!(external: false) }.to change { user.can_create_group }.to(true) .and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit) end end describe 'without external user' do let(:user) { create(:user, external: false) } it 'receives callback when external changes' do expect(user).to receive(:ensure_user_rights_and_limits) user.update!(external: true) end it 'ensures correct rights and limits for user' do expect { user.update!(external: true) }.to change { user.can_create_group }.to(false) .and change { user.projects_limit }.to(0) end end end describe 'feed token' do it 'ensures a feed token on read' do user = create(:user, feed_token: nil) feed_token = user.feed_token expect(feed_token).not_to be_blank expect(user.reload.feed_token).to eq feed_token end it 'ensures no feed token when disabled' do allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true) user = create(:user, feed_token: nil) feed_token = user.feed_token expect(feed_token).to be_blank expect(user.reload.feed_token).to be_blank end end describe 'static object token' do it 'ensures a static object token on read' do user = create(:user, static_object_token: nil) static_object_token = user.static_object_token expect(static_object_token).not_to be_blank expect(user.reload.static_object_token).to eq static_object_token end it 'generates an encrypted version of the token' do user = create(:user, static_object_token: nil) expect(user[:static_object_token]).to be_nil expect(user[:static_object_token_encrypted]).to be_nil user.static_object_token expect(user[:static_object_token]).to be_nil expect(user[:static_object_token_encrypted]).to be_present end it 'prefers an encoded version of the token' do user = create(:user, static_object_token: nil) token = user.static_object_token user.update_column(:static_object_token, 'Test') expect(user.static_object_token).not_to eq('Test') expect(user.static_object_token).to eq(token) end end describe 'enabled_static_object_token' do let_it_be(:static_object_token) { 'ilqx6jm1u945macft4eff0nw' } it 'returns incoming email token when supported' do allow(Gitlab::CurrentSettings).to receive(:static_objects_external_storage_enabled?).and_return(true) user = create(:user, static_object_token: static_object_token) expect(user.enabled_static_object_token).to eq(static_object_token) end it 'returns `nil` when not supported' do allow(Gitlab::CurrentSettings).to receive(:static_objects_external_storage_enabled?).and_return(false) user = create(:user, static_object_token: static_object_token) expect(user.enabled_static_object_token).to be_nil end end describe 'enabled_incoming_email_token' do let_it_be(:incoming_email_token) { 'ilqx6jm1u945macft4eff0nw' } it 'returns incoming email token when supported' do allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(true) user = create(:user, incoming_email_token: incoming_email_token) expect(user.enabled_incoming_email_token).to eq(incoming_email_token) end it 'returns `nil` when not supported' do allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(false) user = create(:user, incoming_email_token: incoming_email_token) expect(user.enabled_incoming_email_token).to be_nil end end describe '#recently_sent_password_reset?' do it 'is false when reset_password_sent_at is nil' do user = build_stubbed(:user, reset_password_sent_at: nil) expect(user.recently_sent_password_reset?).to eq false end it 'is false when sent more than one minute ago' do user = build_stubbed(:user, reset_password_sent_at: 5.minutes.ago) expect(user.recently_sent_password_reset?).to eq false end it 'is true when sent less than one minute ago' do user = build_stubbed(:user, reset_password_sent_at: Time.current) expect(user.recently_sent_password_reset?).to eq true end end describe '#disable_two_factor!' do it 'clears all 2FA-related fields' do user = create(:user, :two_factor) expect(user).to be_two_factor_enabled expect(user.encrypted_otp_secret).not_to be_nil expect(user.otp_backup_codes).not_to be_nil expect(user.otp_grace_period_started_at).not_to be_nil user.disable_two_factor! expect(user).not_to be_two_factor_enabled expect(user.encrypted_otp_secret).to be_nil expect(user.encrypted_otp_secret_iv).to be_nil expect(user.encrypted_otp_secret_salt).to be_nil expect(user.otp_backup_codes).to be_nil expect(user.otp_grace_period_started_at).to be_nil end end describe '#two_factor_otp_enabled?' do let_it_be(:user) { create(:user) } context 'when 2FA is enabled by an MFA Device' do let(:user) { create(:user, :two_factor) } it { expect(user.two_factor_otp_enabled?).to eq(true) } end context 'FortiAuthenticator' do context 'when enabled via GitLab settings' do before do allow(::Gitlab.config.forti_authenticator).to receive(:enabled).and_return(true) end context 'when feature is disabled for the user' do before do stub_feature_flags(forti_authenticator: false) end it { expect(user.two_factor_otp_enabled?).to eq(false) } end context 'when feature is enabled for the user' do before do stub_feature_flags(forti_authenticator: user) end it { expect(user.two_factor_otp_enabled?).to eq(true) } end end context 'when disabled via GitLab settings' do before do allow(::Gitlab.config.forti_authenticator).to receive(:enabled).and_return(false) end it { expect(user.two_factor_otp_enabled?).to eq(false) } end end context 'FortiTokenCloud' do context 'when enabled via GitLab settings' do before do allow(::Gitlab.config.forti_token_cloud).to receive(:enabled).and_return(true) end context 'when feature is disabled for the user' do before do stub_feature_flags(forti_token_cloud: false) end it { expect(user.two_factor_otp_enabled?).to eq(false) } end context 'when feature is enabled for the user' do before do stub_feature_flags(forti_token_cloud: user) end it { expect(user.two_factor_otp_enabled?).to eq(true) } end end context 'when disabled via GitLab settings' do before do allow(::Gitlab.config.forti_token_cloud).to receive(:enabled).and_return(false) end it { expect(user.two_factor_otp_enabled?).to eq(false) } end end end context 'two_factor_u2f_enabled?' do let_it_be(:user) { create(:user, :two_factor) } context 'when webauthn feature flag is enabled' do context 'user has no U2F registration' do it { expect(user.two_factor_u2f_enabled?).to eq(false) } end context 'user has existing U2F registration' do it 'returns false' do device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) create(:u2f_registration, name: 'my u2f device', user: user, certificate: Base64.strict_encode64(device.cert_raw), key_handle: U2F.urlsafe_encode64(device.key_handle_raw), public_key: Base64.strict_encode64(device.origin_public_key_raw)) expect(user.two_factor_u2f_enabled?).to eq(false) end end end context 'when webauthn feature flag is disabled' do before do stub_feature_flags(webauthn: false) end context 'user has no U2F registration' do it { expect(user.two_factor_u2f_enabled?).to eq(false) } end context 'user has existing U2F registration' do it 'returns true' do device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) create(:u2f_registration, name: 'my u2f device', user: user, certificate: Base64.strict_encode64(device.cert_raw), key_handle: U2F.urlsafe_encode64(device.key_handle_raw), public_key: Base64.strict_encode64(device.origin_public_key_raw)) expect(user.two_factor_u2f_enabled?).to eq(true) end end end end describe 'needs_new_otp_secret?', :freeze_time do let(:user) { create(:user) } context 'when two-factor is not enabled' do it 'returns true if otp_secret_expires_at is nil' do expect(user.needs_new_otp_secret?).to eq(true) end it 'returns true if the otp_secret_expires_at has passed' do user.update!(otp_secret_expires_at: 10.minutes.ago) expect(user.reload.needs_new_otp_secret?).to eq(true) end it 'returns false if the otp_secret_expires_at has not passed' do user.update!(otp_secret_expires_at: 10.minutes.from_now) expect(user.reload.needs_new_otp_secret?).to eq(false) end end context 'when two-factor is enabled' do let(:user) { create(:user, :two_factor) } it 'returns false even if ttl is expired' do user.otp_secret_expires_at = 10.minutes.ago expect(user.needs_new_otp_secret?).to eq(false) end end end describe 'otp_secret_expired?', :freeze_time do let(:user) { create(:user) } it 'returns true if otp_secret_expires_at is nil' do expect(user.otp_secret_expired?).to eq(true) end it 'returns true if the otp_secret_expires_at has passed' do user.otp_secret_expires_at = 10.minutes.ago expect(user.otp_secret_expired?).to eq(true) end it 'returns false if the otp_secret_expires_at has not passed' do user.otp_secret_expires_at = 20.minutes.from_now expect(user.otp_secret_expired?).to eq(false) end end describe 'update_otp_secret!', :freeze_time do let(:user) { create(:user) } before do user.update_otp_secret! end it 'sets the otp_secret' do expect(user.otp_secret).to have_attributes(length: described_class::OTP_SECRET_LENGTH) end it 'updates the otp_secret_expires_at' do expect(user.otp_secret_expires_at).to eq(Time.current + described_class::OTP_SECRET_TTL) end end describe 'projects' do before do @user = create(:user) @project = create(:project, namespace: @user.namespace) @project_2 = create(:project, group: create(:group)) do |project| project.add_maintainer(@user) end @project_3 = create(:project, group: create(:group)) do |project| project.add_developer(@user) end end it { expect(@user.authorized_projects).to include(@project) } it { expect(@user.authorized_projects).to include(@project_2) } it { expect(@user.authorized_projects).to include(@project_3) } it { expect(@user.owned_projects).to include(@project) } it { expect(@user.owned_projects).not_to include(@project_2) } it { expect(@user.owned_projects).not_to include(@project_3) } it { expect(@user.personal_projects).to include(@project) } it { expect(@user.personal_projects).not_to include(@project_2) } it { expect(@user.personal_projects).not_to include(@project_3) } end describe 'groups' do let(:user) { create(:user) } let(:group) { create(:group) } before do group.add_owner(user) end it { expect(user.several_namespaces?).to be_truthy } it { expect(user.authorized_groups).to eq([group]) } it { expect(user.owned_groups).to eq([group]) } it { expect(user.namespaces).to contain_exactly(user.namespace, group) } it { expect(user.forkable_namespaces).to contain_exactly(user.namespace, group) } context 'with owned groups only' do before do other_group = create(:group) other_group.add_developer(user) end it { expect(user.namespaces(owned_only: true)).to contain_exactly(user.namespace, group) } end context 'with child groups' do let!(:subgroup) { create(:group, parent: group) } describe '#forkable_namespaces' do it 'includes all the namespaces the user can fork into' do developer_group = create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) developer_group.add_developer(user) expect(user.forkable_namespaces).to contain_exactly(user.namespace, group, subgroup, developer_group) end end describe '#manageable_groups' do shared_examples 'manageable groups examples' do it 'includes all the namespaces the user can manage' do expect(user.manageable_groups).to contain_exactly(group, subgroup) end it 'does not include duplicates if a membership was added for the subgroup' do subgroup.add_owner(user) expect(user.manageable_groups).to contain_exactly(group, subgroup) end end it_behaves_like 'manageable groups examples' context 'when feature flag :linear_user_manageable_groups is disabled' do before do stub_feature_flags(linear_user_manageable_groups: false) end it_behaves_like 'manageable groups examples' end end describe '#manageable_groups_with_routes' do it 'eager loads routes from manageable groups' do control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do user.manageable_groups_with_routes.map(&:route) end.count create(:group, parent: subgroup) expect do user.manageable_groups_with_routes.map(&:route) end.not_to exceed_all_query_limit(control_count) end end end end describe 'group multiple owners' do before do @user = create :user @user2 = create :user @group = create :group @group.add_owner(@user) @group.add_member(@user2, GroupMember::OWNER) end it { expect(@user2.several_namespaces?).to be_truthy } end describe 'namespaced' do before do @user = create :user @project = create(:project, namespace: @user.namespace) end it { expect(@user.several_namespaces?).to be_falsey } it { expect(@user.namespaces).to eq([@user.namespace]) } end describe 'blocking user' do let_it_be_with_refind(:user) { create(:user, name: 'John Smith') } it 'blocks user' do user.block expect(user.blocked?).to be_truthy end context 'when user has running CI pipelines' do let(:pipelines) { build_list(:ci_pipeline, 3, :running) } it 'drops all running pipelines and related jobs' do drop_service = double disable_service = double expect(user).to receive(:pipelines).and_return(pipelines) expect(Ci::DropPipelineService).to receive(:new).and_return(drop_service) expect(drop_service).to receive(:execute_async_for_all).with(pipelines, :user_blocked, user) expect(Ci::DisableUserPipelineSchedulesService).to receive(:new).and_return(disable_service) expect(disable_service).to receive(:execute).with(user) user.block! end it 'does not drop running pipelines if the transaction rolls back' do expect(Ci::DropPipelineService).not_to receive(:new) expect(Ci::DisableUserPipelineSchedulesService).not_to receive(:new) User.transaction do user.block raise ActiveRecord::Rollback end end end context 'when user has active CI pipeline schedules' do let_it_be(:schedule) { create(:ci_pipeline_schedule, active: true, owner: user) } it 'disables any pipeline schedules' do expect { user.block }.to change { schedule.reload.active? }.to(false) end end end describe 'deactivating a user' do let(:user) { create(:user, name: 'John Smith') } context 'an active user' do it 'can be deactivated' do user.deactivate expect(user.deactivated?).to be_truthy end context 'when user deactivation emails are disabled' do before do stub_application_setting(user_deactivation_emails_enabled: false) end it 'does not send deactivated user an email' do expect(NotificationService).not_to receive(:new) user.deactivate end end context 'when user deactivation emails are enabled' do it 'sends deactivated user an email' do expect_next_instance_of(NotificationService) do |notification| allow(notification).to receive(:user_deactivated).with(user.name, user.notification_email_or_default) end user.deactivate end end end context 'a user who is blocked' do before do user.block end it 'cannot be deactivated' do user.deactivate expect(user.reload.deactivated?).to be_falsy end end end describe 'blocking a user pending approval' do let(:user) { create(:user) } before do user.block_pending_approval end context 'an active user' do it 'can be blocked pending approval' do expect(user.blocked_pending_approval?).to eq(true) end it 'behaves like a blocked user' do expect(user.blocked?).to eq(true) end end end describe '.instance_access_request_approvers_to_be_notified' do let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) } it 'returns up to the ten most recently active instance admins' do active_admins_in_recent_sign_in_desc_order = User.admins.active.order_recent_sign_in.limit(10) expect(User.instance_access_request_approvers_to_be_notified).to eq(active_admins_in_recent_sign_in_desc_order) end end describe 'banning and unbanning a user', :aggregate_failures do let(:user) { create(:user) } context 'banning a user' do it 'bans and blocks the user' do user.ban expect(user.banned?).to eq(true) expect(user.blocked?).to eq(true) end it 'creates a BannedUser record' do expect { user.ban }.to change { Users::BannedUser.count }.by(1) expect(Users::BannedUser.last.user_id).to eq(user.id) end end context 'unbanning a user' do before do user.ban! end it 'unbans the user' do user.unban expect(user.banned?).to eq(false) expect(user.active?).to eq(true) end it 'deletes the BannedUser record' do expect { user.unban }.to change { Users::BannedUser.count }.by(-1) expect(Users::BannedUser.where(user_id: user.id)).not_to exist end end end describe '.filter_items' do let(:user) { double } it 'filters by active users by default' do expect(described_class).to receive(:active_without_ghosts).and_return([user]) expect(described_class.filter_items(nil)).to include user end it 'filters by admins' do expect(described_class).to receive(:admins).and_return([user]) expect(described_class.filter_items('admins')).to include user end it 'filters by blocked' do expect(described_class).to receive(:blocked).and_return([user]) expect(described_class.filter_items('blocked')).to include user end it 'filters by banned' do expect(described_class).to receive(:banned).and_return([user]) expect(described_class.filter_items('banned')).to include user end it 'filters by blocked pending approval' do expect(described_class).to receive(:blocked_pending_approval).and_return([user]) expect(described_class.filter_items('blocked_pending_approval')).to include user end it 'filters by deactivated' do expect(described_class).to receive(:deactivated).and_return([user]) expect(described_class.filter_items('deactivated')).to include user end it 'filters by two_factor_disabled' do expect(described_class).to receive(:without_two_factor).and_return([user]) expect(described_class.filter_items('two_factor_disabled')).to include user end it 'filters by two_factor_enabled' do expect(described_class).to receive(:with_two_factor).and_return([user]) expect(described_class.filter_items('two_factor_enabled')).to include user end it 'filters by wop' do expect(described_class).to receive(:without_projects).and_return([user]) expect(described_class.filter_items('wop')).to include user end end describe '.without_projects' do let!(:project) { create(:project, :public) } let!(:user) { create(:user) } let!(:user_without_project) { create(:user) } let!(:user_without_project2) { create(:user) } before do # add user to project project.add_maintainer(user) # create invite to project create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com') # create request to join project project.request_access(user_without_project2) end it { expect(described_class.without_projects).not_to include user } it { expect(described_class.without_projects).to include user_without_project } it { expect(described_class.without_projects).to include user_without_project2 } end describe 'user creation' do describe 'normal user' do let(:user) { create(:user, name: 'John Smith') } it { expect(user.admin?).to be_falsey } it { expect(user.require_ssh_key?).to be_truthy } it { expect(user.can_create_group?).to be_truthy } it { expect(user.can_create_project?).to be_truthy } it { expect(user.first_name).to eq('John') } it { expect(user.external).to be_falsey } end describe 'with defaults' do let(:user) { described_class.new } it 'applies defaults to user' do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey expect(user.private_profile).to eq(false) end end describe 'with default overrides' do let(:user) { described_class.new(projects_limit: 123, can_create_group: false, can_create_team: true) } it 'applies defaults to user' do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey expect(user.theme_id).to eq(1) end it 'does not undo projects_limit setting if it matches old DB default of 10' do # If the real default project limit is 10 then this test is worthless expect(Gitlab.config.gitlab.default_projects_limit).not_to eq(10) user = described_class.new(projects_limit: 10) expect(user.projects_limit).to eq(10) end end context 'when Gitlab::CurrentSettings.user_default_external is true' do before do stub_application_setting(user_default_external: true) end it 'creates external user by default' do user = create(:user) expect(user.external).to be_truthy expect(user.can_create_group).to be_falsey expect(user.projects_limit).to be 0 end describe 'with default overrides' do it 'creates a non-external user' do user = create(:user, external: false) expect(user.external).to be_falsey end end end describe '#require_ssh_key?', :use_clean_rails_memory_store_caching do protocol_and_expectation = { 'http' => false, 'ssh' => true, '' => true } protocol_and_expectation.each do |protocol, expected| it 'has correct require_ssh_key?' do stub_application_setting(enabled_git_access_protocol: protocol) user = build(:user) expect(user.require_ssh_key?).to eq(expected) end end it 'returns false when the user has 1 or more SSH keys' do key = create(:personal_key) expect(key.user.require_ssh_key?).to eq(false) end end end describe '.find_for_database_authentication' do it 'strips whitespace from login' do user = create(:user) expect(described_class.find_for_database_authentication({ login: " #{user.username} " })).to eq user end end describe '.find_by_any_email' do it 'finds user through private commit email' do user = create(:user) private_email = user.private_commit_email expect(described_class.find_by_any_email(private_email)).to eq(user) expect(described_class.find_by_any_email(private_email, confirmed: true)).to eq(user) end it 'finds user through private commit email when user is unconfirmed' do user = create(:user, :unconfirmed) private_email = user.private_commit_email expect(described_class.find_by_any_email(private_email)).to eq(user) expect(described_class.find_by_any_email(private_email, confirmed: true)).to eq(user) end it 'finds by primary email' do user = create(:user, email: 'foo@example.com') expect(described_class.find_by_any_email(user.email)).to eq user expect(described_class.find_by_any_email(user.email, confirmed: true)).to eq user end it 'finds by primary email when user is unconfirmed according to confirmed argument' do user = create(:user, :unconfirmed, email: 'foo@example.com') expect(described_class.find_by_any_email(user.email)).to eq user expect(described_class.find_by_any_email(user.email, confirmed: true)).to be_nil end it 'finds by uppercased email' do user = create(:user, email: 'foo@example.com') expect(described_class.find_by_any_email(user.email.upcase)).to eq user expect(described_class.find_by_any_email(user.email.upcase, confirmed: true)).to eq user end context 'finds by secondary email' do context 'when primary email is confirmed' do let(:user) { email.user } context 'when secondary email is confirmed' do let!(:email) { create(:email, :confirmed, email: 'foo@example.com') } it 'finds user' do expect(described_class.find_by_any_email(email.email)).to eq user expect(described_class.find_by_any_email(email.email, confirmed: true)).to eq user end end context 'when secondary email is unconfirmed' do let!(:email) { create(:email, email: 'foo@example.com') } it 'does not find user' do expect(described_class.find_by_any_email(email.email)).to be_nil expect(described_class.find_by_any_email(email.email, confirmed: true)).to be_nil end end end context 'when primary email is unconfirmed' do let(:user) { create(:user, :unconfirmed) } context 'when secondary email is confirmed' do let!(:email) { create(:email, :confirmed, user: user, email: 'foo@example.com') } it 'finds user according to confirmed argument' do expect(described_class.find_by_any_email(email.email)).to eq user expect(described_class.find_by_any_email(email.email, confirmed: true)).to be_nil end end context 'when secondary email is unconfirmed' do let!(:email) { create(:email, user: user, email: 'foo@example.com') } it 'does not find user' do expect(described_class.find_by_any_email(email.email)).to be_nil expect(described_class.find_by_any_email(email.email, confirmed: true)).to be_nil end end end end it 'returns nil when nothing found' do expect(described_class.find_by_any_email('')).to be_nil end end describe '.by_any_email' do it 'returns an ActiveRecord::Relation' do expect(described_class.by_any_email('foo@example.com')) .to be_a_kind_of(ActiveRecord::Relation) end it 'returns empty relation of users when nothing found' do expect(described_class.by_any_email('')).to be_empty end it 'returns a relation of users for confirmed primary emails' do user = create(:user) expect(described_class.by_any_email(user.email)).to match_array([user]) expect(described_class.by_any_email(user.email, confirmed: true)).to match_array([user]) end it 'returns a relation of users for unconfirmed primary emails according to confirmed argument' do user = create(:user, :unconfirmed) expect(described_class.by_any_email(user.email)).to match_array([user]) expect(described_class.by_any_email(user.email, confirmed: true)).to be_empty end it 'finds users through private commit emails' do user = create(:user) private_email = user.private_commit_email expect(described_class.by_any_email(private_email)).to match_array([user]) expect(described_class.by_any_email(private_email, confirmed: true)).to match_array([user]) end it 'finds unconfirmed users through private commit emails' do user = create(:user, :unconfirmed) private_email = user.private_commit_email expect(described_class.by_any_email(private_email)).to match_array([user]) expect(described_class.by_any_email(private_email, confirmed: true)).to match_array([user]) end it 'finds user through a private commit email in an array' do user = create(:user) private_email = user.private_commit_email expect(described_class.by_any_email([private_email])).to match_array([user]) expect(described_class.by_any_email([private_email], confirmed: true)).to match_array([user]) end it 'finds by uppercased email' do user = create(:user, email: 'foo@example.com') expect(described_class.by_any_email(user.email.upcase)).to match_array([user]) expect(described_class.by_any_email(user.email.upcase, confirmed: true)).to match_array([user]) end context 'finds by secondary email' do context 'when primary email is confirmed' do let(:user) { email.user } context 'when secondary email is confirmed' do let!(:email) { create(:email, :confirmed, email: 'foo@example.com') } it 'finds user' do expect(described_class.by_any_email(email.email)).to match_array([user]) expect(described_class.by_any_email(email.email, confirmed: true)).to match_array([user]) end end context 'when secondary email is unconfirmed' do let!(:email) { create(:email, email: 'foo@example.com') } it 'does not find user' do expect(described_class.by_any_email(email.email)).to be_empty expect(described_class.by_any_email(email.email, confirmed: true)).to be_empty end end end context 'when primary email is unconfirmed' do let(:user) { create(:user, :unconfirmed) } context 'when secondary email is confirmed' do let!(:email) { create(:email, :confirmed, user: user, email: 'foo@example.com') } it 'finds user according to confirmed argument' do expect(described_class.by_any_email(email.email)).to match_array([user]) expect(described_class.by_any_email(email.email, confirmed: true)).to be_empty end end context 'when secondary email is unconfirmed' do let!(:email) { create(:email, user: user, email: 'foo@example.com') } it 'does not find user' do expect(described_class.by_any_email(email.email)).to be_empty expect(described_class.by_any_email(email.email, confirmed: true)).to be_empty end end end end end describe '.search' do let_it_be(:user) { create(:user, name: 'user', username: 'usern', email: 'email@example.com') } let_it_be(:public_email) do create(:email, :confirmed, user: user, email: 'publicemail@example.com').tap do |email| user.update!(public_email: email.email) end end let_it_be(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@example.com') } let_it_be(:user3) { create(:user, name: 'us', username: 'se', email: 'foo@example.com') } let_it_be(:unconfirmed_user) { create(:user, :unconfirmed, name: 'not verified', username: 'notverified') } let_it_be(:unconfirmed_secondary_email) { create(:email, user: user, email: 'alias@example.com') } let_it_be(:confirmed_secondary_email) { create(:email, :confirmed, user: user, email: 'alias2@example.com') } describe 'name user and email relative ordering' do let_it_be(:named_alexander) { create(:user, name: 'Alexander Person', username: 'abcd', email: 'abcd@example.com') } let_it_be(:username_alexand) { create(:user, name: 'Joao Alexander', username: 'Alexand', email: 'joao@example.com') } it 'prioritizes exact matches' do expect(described_class.search('Alexand')).to eq([username_alexand, named_alexander]) end it 'falls back to ordering by name' do expect(described_class.search('Alexander')).to eq([named_alexander, username_alexand]) end end describe 'name matching' do it 'returns users with a matching name with exact match first' do expect(described_class.search(user.name)).to eq([user, user2]) end it 'returns users with a partially matching name' do expect(described_class.search(user.name[0..2])).to eq([user, user2]) end it 'returns users with a matching name regardless of the casing' do expect(described_class.search(user2.name.upcase)).to eq([user2]) end it 'returns users with a exact matching name shorter than 3 chars' do expect(described_class.search(user3.name)).to eq([user3]) end it 'returns users with a exact matching name shorter than 3 chars regardless of the casing' do expect(described_class.search(user3.name.upcase)).to eq([user3]) end context 'when use_minimum_char_limit is false' do it 'returns users with a partially matching name' do expect(described_class.search('u', use_minimum_char_limit: false)).to eq([user3, user, user2]) end end end describe 'email matching' do it 'returns users with a matching public email' do expect(described_class.search(user.public_email)).to match_array([user]) end it 'does not return users with a partially matching public email' do expect(described_class.search(user.public_email[1...-1])).to be_empty end it 'returns users with a matching public email regardless of the casing' do expect(described_class.search(user.public_email.upcase)).to match_array([user]) end it 'does not return users with a matching private email' do expect(described_class.search(user.email)).to be_empty expect(described_class.search(unconfirmed_secondary_email.email)).to be_empty expect(described_class.search(confirmed_secondary_email.email)).to be_empty end context 'with private emails search' do it 'returns users with matching private primary email' do expect(described_class.search(user.email, with_private_emails: true)).to match_array([user]) end it 'returns users with matching private unconfirmed primary email' do expect(described_class.search(unconfirmed_user.email, with_private_emails: true)).to match_array([unconfirmed_user]) end it 'returns users with matching private confirmed secondary email' do expect(described_class.search(confirmed_secondary_email.email, with_private_emails: true)).to match_array([user]) end it 'does not return users with matching private unconfirmed secondary email' do expect(described_class.search(unconfirmed_secondary_email.email, with_private_emails: true)).to be_empty end end end describe 'username matching' do it 'returns users with a matching username' do expect(described_class.search(user.username)).to eq([user, user2]) end it 'returns users with a matching username starting with a @' do expect(described_class.search("@#{user.username}")).to eq([user, user2]) end it 'returns users with a partially matching username' do expect(described_class.search(user.username[0..2])).to eq([user, user2]) end it 'returns users with a partially matching username starting with @' do expect(described_class.search("@#{user.username[0..2]}")).to eq([user, user2]) end it 'returns users with a matching username regardless of the casing' do expect(described_class.search(user2.username.upcase)).to eq([user2]) end it 'returns users with a exact matching username shorter than 3 chars' do expect(described_class.search(user3.username)).to eq([user3]) end it 'returns users with a exact matching username shorter than 3 chars regardless of the casing' do expect(described_class.search(user3.username.upcase)).to eq([user3]) end context 'when use_minimum_char_limit is false' do it 'returns users with a partially matching username' do expect(described_class.search('se', use_minimum_char_limit: false)).to eq([user3, user, user2]) end end end it 'returns no matches for an empty string' do expect(described_class.search('')).to be_empty end it 'returns no matches for nil' do expect(described_class.search(nil)).to be_empty end end describe '.user_search_minimum_char_limit' do it 'returns true' do expect(described_class.user_search_minimum_char_limit).to be(true) end end describe '.find_by_ssh_key_id' do let_it_be(:user) { create(:user) } let_it_be(:key) { create(:key, user: user) } context 'using an existing SSH key ID' do it 'returns the corresponding User' do expect(described_class.find_by_ssh_key_id(key.id)).to eq(user) end end it 'only performs a single query' do key # Don't count the queries for creating the key and user expect { described_class.find_by_ssh_key_id(key.id) } .not_to exceed_query_limit(1) end context 'using an invalid SSH key ID' do it 'returns nil' do expect(described_class.find_by_ssh_key_id(-1)).to be_nil end end end shared_examples "find user by login" do let_it_be(:user) { create(:user) } let_it_be(:invalid_login) { "#{user.username}-NOT-EXISTS" } context 'when login is nil or empty' do it 'returns nil' do expect(login_method(nil)).to be_nil expect(login_method('')).to be_nil end end context 'when login is invalid' do it 'returns nil' do expect(login_method(invalid_login)).to be_nil end end context 'when login is username' do it 'returns user' do expect(login_method(user.username)).to eq(user) expect(login_method(user.username.downcase)).to eq(user) expect(login_method(user.username.upcase)).to eq(user) end end context 'when login is email' do it 'returns user' do expect(login_method(user.email)).to eq(user) expect(login_method(user.email.downcase)).to eq(user) expect(login_method(user.email.upcase)).to eq(user) end end end describe '.by_login' do it_behaves_like "find user by login" do def login_method(login) described_class.by_login(login).take end end end describe '.find_by_login' do it_behaves_like "find user by login" do def login_method(login) described_class.find_by_login(login) end end end describe '.find_by_username' do it 'returns nil if not found' do expect(described_class.find_by_username('JohnDoe')).to be_nil end it 'is case-insensitive' do user = create(:user, username: 'JohnDoe') expect(described_class.find_by_username('JOHNDOE')).to eq user end end describe '.find_by_username!' do it 'raises RecordNotFound' do expect { described_class.find_by_username!('JohnDoe') } .to raise_error(ActiveRecord::RecordNotFound) end it 'is case-insensitive' do user = create(:user, username: 'JohnDoe') expect(described_class.find_by_username!('JOHNDOE')).to eq user end end describe '.find_by_full_path' do let!(:user) { create(:user, namespace: create(:user_namespace)) } context 'with a route matching the given path' do let!(:route) { user.namespace.route } it 'returns the user' do expect(described_class.find_by_full_path(route.path)).to eq(user) end it 'is case-insensitive' do expect(described_class.find_by_full_path(route.path.upcase)).to eq(user) expect(described_class.find_by_full_path(route.path.downcase)).to eq(user) end context 'with a redirect route matching the given path' do let!(:redirect_route) { user.namespace.redirect_routes.create!(path: 'foo') } context 'without the follow_redirects option' do it 'returns nil' do expect(described_class.find_by_full_path(redirect_route.path)).to eq(nil) end end context 'with the follow_redirects option set to true' do it 'returns the user' do expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(user) end it 'is case-insensitive' do expect(described_class.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(user) expect(described_class.find_by_full_path(redirect_route.path.downcase, follow_redirects: true)).to eq(user) end end end context 'without a route or a redirect route matching the given path' do context 'without the follow_redirects option' do it 'returns nil' do expect(described_class.find_by_full_path('unknown')).to eq(nil) end end context 'with the follow_redirects option set to true' do it 'returns nil' do expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) end end end context 'with a group route matching the given path' do let!(:group) { create(:group, path: 'group_path') } context 'when the group namespace has an owner_id (legacy data)' do before do group.update!(owner_id: user.id) end it 'returns nil' do expect(described_class.find_by_full_path('group_path')).to eq(nil) end end context 'when the group namespace does not have an owner_id' do it 'returns nil' do expect(described_class.find_by_full_path('group_path')).to eq(nil) end end end end end describe 'all_ssh_keys' do it { is_expected.to have_many(:keys).dependent(:destroy) } it 'has all ssh keys' do user = create :user key = create :key_without_comment, user_id: user.id expect(user.all_ssh_keys).to include(a_string_starting_with(key.key)) end end describe '#avatar_type' do let(:user) { create(:user) } it 'is true if avatar is image' do user.update_attribute(:avatar, 'uploads/avatar.png') expect(user.avatar_type).to be_truthy end it 'is false if avatar is html page' do user.update_attribute(:avatar, 'uploads/avatar.html') user.avatar_type expect(user.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true end end describe '#avatar_url' do let(:user) { create(:user, :with_avatar) } context 'when avatar file is uploaded' do it 'shows correct avatar url' do expect(user.avatar_url).to eq(user.avatar.url) expect(user.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, user.avatar.url].join) end end end describe '#clear_avatar_caches' do let(:user) { create(:user) } it 'clears the avatar cache when saving' do allow(user).to receive(:avatar_changed?).and_return(true) expect(Gitlab::AvatarCache).to receive(:delete_by_email).with(*user.verified_emails) user.update!(avatar: fixture_file_upload('spec/fixtures/dk.png')) end end describe '#accept_pending_invitations!' do let(:user) { create(:user, email: 'user@email.com') } let(:confirmed_secondary_email) { create(:email, :confirmed, email: 'confirmedsecondary@example.com', user: user) } let(:unconfirmed_secondary_email) { create(:email, email: 'unconfirmedsecondary@example.com', user: user) } let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) } let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) } let!(:external_project_member_invite) { create(:project_member, :invited, invite_email: 'external@email.com') } let!(:external_group_member_invite) { create(:group_member, :invited, invite_email: 'external@email.com') } let!(:project_member_invite_via_confirmed_secondary_email) { create(:project_member, :invited, invite_email: confirmed_secondary_email.email) } let!(:group_member_invite_via_confirmed_secondary_email) { create(:group_member, :invited, invite_email: confirmed_secondary_email.email) } let!(:project_member_invite_via_unconfirmed_secondary_email) { create(:project_member, :invited, invite_email: unconfirmed_secondary_email.email) } let!(:group_member_invite_via_unconfirmed_secondary_email) { create(:group_member, :invited, invite_email: unconfirmed_secondary_email.email) } it 'accepts all the user members pending invitations and returns the accepted_members' do accepted_members = user.accept_pending_invitations! expect(accepted_members).to match_array( [ project_member_invite, group_member_invite, project_member_invite_via_confirmed_secondary_email, group_member_invite_via_confirmed_secondary_email ] ) expect(group_member_invite.reload).not_to be_invite expect(project_member_invite.reload).not_to be_invite expect(external_project_member_invite.reload).to be_invite expect(external_group_member_invite.reload).to be_invite expect(project_member_invite_via_confirmed_secondary_email.reload).not_to be_invite expect(group_member_invite_via_confirmed_secondary_email.reload).not_to be_invite expect(project_member_invite_via_unconfirmed_secondary_email.reload).to be_invite expect(group_member_invite_via_unconfirmed_secondary_email.reload).to be_invite end end describe '#all_emails' do let(:user) { create(:user) } let!(:unconfirmed_secondary_email) { create(:email, user: user) } let!(:confirmed_secondary_email) { create(:email, :confirmed, user: user) } it 'returns all emails' do expect(user.all_emails).to contain_exactly( user.email, user.private_commit_email, confirmed_secondary_email.email ) end context 'when the primary email is confirmed' do it 'includes the primary email' do expect(user.all_emails).to include(user.email) end end context 'when the primary email is unconfirmed' do let!(:user) { create(:user, :unconfirmed) } it 'includes the primary email' do expect(user.all_emails).to include(user.email) end end context 'when the primary email is temp email for oauth' do let!(:user) { create(:omniauth_user, :unconfirmed, email: 'temp-email-for-oauth-user@gitlab.localhost') } it 'does not include the primary email' do expect(user.all_emails).not_to include(user.email) end end context 'when `include_private_email` is true' do it 'includes the private commit email' do expect(user.all_emails).to include(user.private_commit_email) end end context 'when `include_private_email` is false' do it 'does not include the private commit email' do expect(user.all_emails(include_private_email: false)).not_to include( user.private_commit_email ) end end context 'when the secondary email is confirmed' do it 'includes the secondary email' do expect(user.all_emails).to include(confirmed_secondary_email.email) end end context 'when the secondary email is unconfirmed' do it 'does not include the secondary email' do expect(user.all_emails).not_to include(unconfirmed_secondary_email.email) end end end describe '#verified_emails' do let(:user) { create(:user) } it 'returns only confirmed emails' do email_confirmed = create :email, user: user, confirmed_at: Time.current create :email, user: user expect(user.verified_emails).to contain_exactly( user.email, user.private_commit_email, email_confirmed.email ) end end describe '#public_verified_emails' do let(:user) { create(:user) } it 'returns only confirmed public emails' do email_confirmed = create :email, user: user, confirmed_at: Time.current create :email, user: user expect(user.public_verified_emails).to contain_exactly( user.email, email_confirmed.email ) end it 'returns confirmed public emails plus main user email when user is not confirmed' do user = create(:user, confirmed_at: nil) email_confirmed = create :email, user: user, confirmed_at: Time.current create :email, user: user expect(user.public_verified_emails).to contain_exactly( user.email, email_confirmed.email ) end end describe '#verified_email?' do let(:user) { create(:user) } it 'returns true when the email is verified/confirmed' do email_confirmed = create :email, user: user, confirmed_at: Time.current create :email, user: user user.reload expect(user.verified_email?(user.email)).to be_truthy expect(user.verified_email?(email_confirmed.email.titlecase)).to be_truthy end it 'returns true when user is found through private commit email' do expect(user.verified_email?(user.private_commit_email)).to be_truthy end it 'returns true for an outdated private commit email' do old_email = user.private_commit_email user.update!(username: 'changed-username') expect(user.verified_email?(old_email)).to be_truthy end it 'returns false when the email is not verified/confirmed' do email_unconfirmed = create :email, user: user user.reload expect(user.verified_email?(email_unconfirmed.email)).to be_falsy end end context 'crowd synchronized user' do describe '#crowd_user?' do it 'is true if provider is crowd' do user = create(:omniauth_user, provider: 'crowd') expect(user.crowd_user?).to be_truthy end it 'is false for other providers' do user = create(:omniauth_user, provider: 'other-provider') expect(user.crowd_user?).to be_falsey end it 'is false if no extern_uid is provided' do user = create(:omniauth_user, extern_uid: nil) expect(user.crowd_user?).to be_falsey end end end describe '#requires_ldap_check?' do let(:user) { described_class.new } it 'is false when LDAP is disabled' do # Create a condition which would otherwise cause 'true' to be returned allow(user).to receive(:ldap_user?).and_return(true) user.last_credential_check_at = nil expect(user.requires_ldap_check?).to be_falsey end context 'when LDAP is enabled' do before do allow(Gitlab.config.ldap).to receive(:enabled).and_return(true) end it 'is false for non-LDAP users' do allow(user).to receive(:ldap_user?).and_return(false) expect(user.requires_ldap_check?).to be_falsey end context 'and when the user is an LDAP user' do before do allow(user).to receive(:ldap_user?).and_return(true) end it 'is true when the user has never had an LDAP check before' do user.last_credential_check_at = nil expect(user.requires_ldap_check?).to be_truthy end it 'is true when the last LDAP check happened over 1 hour ago' do user.last_credential_check_at = 2.hours.ago expect(user.requires_ldap_check?).to be_truthy end end end end context 'ldap synchronized user' do describe '#ldap_user?' do it 'is true if provider name starts with ldap' do user = create(:omniauth_user, provider: 'ldapmain') expect(user.ldap_user?).to be_truthy end it 'is false for other providers' do user = create(:omniauth_user, provider: 'other-provider') expect(user.ldap_user?).to be_falsey end it 'is false if no extern_uid is provided' do user = create(:omniauth_user, extern_uid: nil) expect(user.ldap_user?).to be_falsey end end describe '#ldap_identity' do it 'returns ldap identity' do user = create(:omniauth_user, :ldap) expect(user.ldap_identity.provider).not_to be_empty end end describe '#matches_identity?' do it 'finds the identity when the DN is formatted differently' do user = create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com') expect(user.matches_identity?('ldapmain', 'uid=John Smith, ou=People, dc=example, dc=com')).to eq(true) end end describe '#ldap_block' do let(:user) { create(:omniauth_user, provider: 'ldapmain', name: 'John Smith') } it 'blocks user flaging the action caming from ldap' do user.ldap_block expect(user.blocked?).to be_truthy expect(user.ldap_blocked?).to be_truthy end context 'on a read-only instance' do before do allow(Gitlab::Database).to receive(:read_only?).and_return(true) end it 'does not block user' do user.ldap_block expect(user.blocked?).to be_falsey expect(user.ldap_blocked?).to be_falsey end end end end describe '#full_website_url' do let(:user) { create(:user) } it 'begins with http if website url omits it' do user.website_url = 'test.com' expect(user.full_website_url).to eq 'http://test.com' end it 'begins with http if website url begins with http' do user.website_url = 'http://test.com' expect(user.full_website_url).to eq 'http://test.com' end it 'begins with https if website url begins with https' do user.website_url = 'https://test.com' expect(user.full_website_url).to eq 'https://test.com' end end describe '#short_website_url' do let(:user) { create(:user) } it 'does not begin with http if website url omits it' do user.website_url = 'test.com' expect(user.short_website_url).to eq 'test.com' end it 'does not begin with http if website url begins with http' do user.website_url = 'http://test.com' expect(user.short_website_url).to eq 'test.com' end it 'does not begin with https if website url begins with https' do user.website_url = 'https://test.com' expect(user.short_website_url).to eq 'test.com' end end describe '#sanitize_attrs' do let(:user) { build(:user, name: 'test <& user', skype: 'test&user') } it 'encodes HTML entities in the Skype attribute' do expect { user.sanitize_attrs }.to change { user.skype }.to('test&user') end it 'does not encode HTML entities in the name attribute' do expect { user.sanitize_attrs }.not_to change { user.name } end it 'sanitizes attr from html tags' do user = create(:user, name: 'Test', twitter: 'https://twitter.com') expect(user.name).to eq('Test') expect(user.twitter).to eq('https://twitter.com') end it 'sanitizes attr from js scripts' do user = create(:user, name: '') expect(user.name).to eq("alert(\"Test\")") end it 'sanitizes attr from iframe scripts' do user = create(:user, name: 'User">