gitlab-org--gitlab-foss/spec/models/user_spec.rb

1917 lines
60 KiB
Ruby
Raw Normal View History

2011-10-08 17:36:38 -04:00
require 'spec_helper'
2015-12-09 04:50:51 -05:00
describe User, models: true do
include Gitlab::CurrentSettings
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Gitlab::ConfigHelper) }
it { is_expected.to include_module(Gitlab::CurrentSettings) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(TokenAuthenticatable) }
end
describe 'delegations' do
it { is_expected.to delegate_method(:path).to(:namespace).with_prefix }
end
describe 'associations' do
it { is_expected.to have_one(:namespace) }
it { is_expected.to have_many(:snippets).dependent(:destroy) }
it { is_expected.to have_many(:project_members).dependent(:destroy) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:recent_events).class_name('Event') }
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) }
2016-01-26 14:55:33 -05:00
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
2016-02-20 08:59:59 -05:00
it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
2017-03-05 12:23:42 -05:00
it { is_expected.to have_many(:triggers).dependent(:destroy) }
2016-07-15 09:42:29 -04:00
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
2016-11-16 08:56:30 -05:00
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
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, :access_requestable)
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(:empty_project, :public, :access_requestable)
project.request_access(user)
expect(user.project_members).to be_empty
end
end
end
describe 'validations' do
2015-12-07 16:17:12 -05:00
describe 'username' do
it 'validates presence' do
expect(subject).to validate_presence_of(:username)
end
it 'rejects blacklisted names' do
user = build(:user, username: 'dashboard')
expect(user).not_to be_valid
expect(user.errors.values).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
2015-12-07 16:17:12 -05:00
it 'validates uniqueness' do
expect(subject).to validate_uniqueness_of(:username).case_insensitive
2015-12-07 16:17:12 -05:00
end
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 { is_expected.to validate_length_of(:bio).is_at_most(255) }
it_behaves_like 'an object with email-formated attributes', :email do
subject { build(:user) }
end
2014-06-11 07:22:56 -04:00
it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do
subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } }
end
describe 'email' do
context 'when no signup domains whitelisted' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return([])
end
it 'accepts any email' do
user = build(:user, email: "info@example.com")
expect(user).to be_valid
end
end
context 'when a signup domain is whitelisted and subdomains are allowed' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['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
end
end
context 'when a signup domain is whitelisted and subdomains are not allowed' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['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
end
it 'rejects example@test.com' do
user = build(:user, email: "example@test.com")
expect(user).to be_invalid
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 blacklist' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist_enabled?).and_return(true)
allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['example.com'])
end
context 'when a signup domain is blacklisted' 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
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
end
context 'when a signup domain is blacklisted but a wildcard subdomain is allowed' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['test.example.com'])
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['*.example.com'])
end
it 'gives priority to whitelist 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
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['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
end
end
end
context 'owns_notification_email' do
it 'accepts temp_oauth_email emails' do
user = build(:user, email: "temp-email-for-oauth@example.com")
expect(user).to be_valid
end
end
end
end
describe "scopes" do
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 = User.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 U2F" do
user_with_2fa = create(:user, :two_factor_via_u2f)
user_without_2fa = create(:user)
users_with_two_factor = User.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 U2F" do
user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
user_without_2fa = create(:user)
users_with_two_factor = User.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
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 = User.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 U2F" do
user_with_2fa = create(:user, :two_factor_via_u2f)
user_without_2fa = create(:user)
users_without_two_factor = User.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 = User.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
2016-11-03 06:22:56 -04:00
describe '.todo_authors' do
it 'filters users' do
create :user
user_2 = create :user
user_3 = create :user
current_user = create :user
create(:todo, user: current_user, author: user_2, state: :done)
create(:todo, user: current_user, author: user_3, state: :pending)
expect(User.todo_authors(current_user.id, 'pending')).to eq [user_3]
expect(User.todo_authors(current_user.id, 'done')).to eq [user_2]
end
end
2011-10-08 17:36:38 -04:00
end
describe "Respond to" do
2017-04-08 22:20:57 -04:00
it { is_expected.to respond_to(:admin?) }
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:private_token) }
it { is_expected.to respond_to(:external?) }
end
describe 'before save hook' do
context 'when saving an external user' do
let(:user) { create(:user) }
let(:external_user) { create(:user, external: true) }
it "sets other properties aswell" 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
2011-10-08 17:36:38 -04:00
end
describe '#update_tracked_fields!', :redis do
let(:request) { OpenStruct.new(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
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 '#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
before do
allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
end
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') }
it 'returns unconfirmed' do
expect(user.confirmed?).to be_falsey
end
it 'confirms a user' do
user.confirm
expect(user.confirmed?).to be_truthy
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
user = create(:user, password: 'abcdefghe')
expect(user.password).to eq('abcdefghe')
end
end
describe 'authentication token' do
it "has authentication token" do
user = create(:user)
expect(user.authentication_token).not_to be_blank
end
2011-11-15 02:08:05 -05:00
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
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_attributes(external: false)
end
it 'ensures correct rights and limits for user' do
2017-06-19 10:21:25 -04:00
stub_config_setting(default_can_create_group: true)
expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true)
.and change { user.projects_limit }.to(current_application_settings.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_attributes(external: true)
end
it 'ensures correct rights and limits for user' do
expect { user.update_attributes(external: true) }.to change { user.can_create_group }.to(false)
.and change { user.projects_limit }.to(0)
end
end
end
2017-05-23 08:09:19 -04:00
describe 'rss token' do
it 'ensures an rss token on read' do
user = create(:user, rss_token: nil)
rss_token = user.rss_token
expect(rss_token).not_to be_blank
expect(user.reload.rss_token).to eq rss_token
2017-05-23 08:09:19 -04:00
end
end
describe '#recently_sent_password_reset?' do
2015-10-01 21:41:56 -04:00
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.now)
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 'projects' do
before do
@user = create(:user)
@project = create(:empty_project, namespace: @user.namespace)
@project_2 = create(:empty_project, group: create(:group)) do |project|
project.add_master(@user)
end
@project_3 = create(:empty_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
before do
@user = create :user
2013-09-26 07:52:17 -04:00
@group = create :group
@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 match_array([@user.namespace, @group]) }
end
describe 'group multiple owners' do
before do
@user = create :user
@user2 = create :user
2013-09-26 07:52:17 -04:00
@group = create :group
@group.add_owner(@user)
@group.add_user(@user2, GroupMember::OWNER)
end
it { expect(@user2.several_namespaces?).to be_truthy }
end
describe 'namespaced' do
before do
@user = create :user
@project = create(:empty_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(:user) { create(:user, name: 'John Smith') }
it "blocks user" do
user.block
expect(user.blocked?).to be_truthy
end
end
describe '.filter' do
let(:user) { double }
it 'filters by active users by default' do
expect(User).to receive(:active).and_return([user])
expect(User.filter(nil)).to include user
end
it 'filters by admins' do
expect(User).to receive(:admins).and_return([user])
expect(User.filter('admins')).to include user
end
it 'filters by blocked' do
expect(User).to receive(:blocked).and_return([user])
expect(User.filter('blocked')).to include user
end
it 'filters by two_factor_disabled' do
expect(User).to receive(:without_two_factor).and_return([user])
expect(User.filter('two_factor_disabled')).to include user
end
it 'filters by two_factor_enabled' do
expect(User).to receive(:with_two_factor).and_return([user])
expect(User.filter('two_factor_enabled')).to include user
end
it 'filters by wop' do
expect(User).to receive(:without_projects).and_return([user])
expect(User.filter('wop')).to include user
end
end
2016-11-16 04:49:45 -05:00
describe '.without_projects' do
let!(:project) { create(:empty_project, :public, :access_requestable) }
2016-11-16 04:49:45 -05:00
let!(:user) { create(:user) }
let!(:user_without_project) { create(:user) }
let!(:user_without_project2) { create(:user) }
before do
# add user to project
project.team << [user, :master]
# create invite to projet
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(User.without_projects).not_to include user }
it { expect(User.without_projects).to include user_without_project }
it { expect(User.without_projects).to include user_without_project2 }
end
describe 'user creation' do
describe 'normal user' do
let(:user) { create(:user, name: 'John Smith') }
2013-01-02 16:35:11 -05:00
2017-04-08 22:20:57 -04:00
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) { User.new }
2013-09-14 15:01:31 -04:00
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.external).to be_falsey
end
end
describe 'with default overrides' do
let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true) }
2013-09-14 15:01:31 -04:00
it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
end
end
context 'when current_application_settings.user_default_external is true' do
before do
stub_application_setting(user_default_external: true)
end
it "creates external user by default" do
user = build(:user)
expect(user.external).to be_truthy
end
describe 'with default overrides' do
it "creates a non-external user" do
user = build(:user, external: false)
expect(user.external).to be_falsey
end
end
end
describe '#require_ssh_key?' 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
end
end
describe '.find_by_any_email' do
it 'finds by primary email' do
user = create(:user, email: 'foo@example.com')
expect(User.find_by_any_email(user.email)).to eq user
end
it 'finds by secondary email' do
email = create(:email, email: 'foo@example.com')
user = email.user
expect(User.find_by_any_email(email.email)).to eq user
end
it 'returns nil when nothing found' do
expect(User.find_by_any_email('')).to be_nil
end
end
2016-03-01 10:08:48 -05:00
describe '.search' do
let(:user) { create(:user) }
it 'returns users with a matching name' do
expect(described_class.search(user.name)).to eq([user])
end
it 'returns users with a partially matching name' do
expect(described_class.search(user.name[0..2])).to eq([user])
end
2016-03-04 05:54:37 -05:00
it 'returns users with a matching name regardless of the casing' do
2016-03-01 10:08:48 -05:00
expect(described_class.search(user.name.upcase)).to eq([user])
end
it 'returns users with a matching Email' do
expect(described_class.search(user.email)).to eq([user])
end
it 'returns users with a partially matching Email' do
expect(described_class.search(user.email[0..2])).to eq([user])
end
2016-03-04 05:54:37 -05:00
it 'returns users with a matching Email regardless of the casing' do
2016-03-01 10:08:48 -05:00
expect(described_class.search(user.email.upcase)).to eq([user])
end
it 'returns users with a matching username' do
expect(described_class.search(user.username)).to eq([user])
end
it 'returns users with a partially matching username' do
expect(described_class.search(user.username[0..2])).to eq([user])
end
2016-03-04 05:54:37 -05:00
it 'returns users with a matching username regardless of the casing' do
2016-03-01 10:08:48 -05:00
expect(described_class.search(user.username.upcase)).to eq([user])
2014-03-26 07:44:00 -04:00
end
end
describe '.search_with_secondary_emails' do
2017-02-22 12:51:46 -05:00
delegate :search_with_secondary_emails, to: :described_class
let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) }
let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) }
let!(:email) do
create(:email, user: another_user, email: 'alias@example.com')
end
it 'returns users with a matching name' do
expect(search_with_secondary_emails(user.name)).to eq([user])
end
it 'returns users with a partially matching name' do
expect(search_with_secondary_emails(user.name[0..2])).to eq([user])
end
it 'returns users with a matching name regardless of the casing' do
expect(search_with_secondary_emails(user.name.upcase)).to eq([user])
end
it 'returns users with a matching email' do
expect(search_with_secondary_emails(user.email)).to eq([user])
end
it 'returns users with a partially matching email' do
expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
end
it 'returns users with a matching email regardless of the casing' do
expect(search_with_secondary_emails(user.email.upcase)).to eq([user])
end
it 'returns users with a matching username' do
expect(search_with_secondary_emails(user.username)).to eq([user])
end
it 'returns users with a partially matching username' do
expect(search_with_secondary_emails(user.username[0..2])).to eq([user])
end
it 'returns users with a matching username regardless of the casing' do
expect(search_with_secondary_emails(user.username.upcase)).to eq([user])
end
it 'returns users with a matching whole secondary email' do
expect(search_with_secondary_emails(email.email)).to eq([email.user])
end
it 'returns users with a matching part of secondary email' do
expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
end
it 'return users with a matching part of secondary email regardless of case' do
expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
end
it 'returns multiple users with matching secondary emails' do
email1 = create(:email, email: '1_testemail@example.com')
email2 = create(:email, email: '2_testemail@example.com')
email3 = create(:email, email: 'other@email.com')
email3.user.update_attributes!(email: 'another@mail.com')
expect(
search_with_secondary_emails('testemail@example.com').map(&:id)
).to include(email1.user.id, email2.user.id)
expect(
search_with_secondary_emails('testemail@example.com').map(&:id)
).not_to include(email3.user.id)
end
2014-03-26 07:44:00 -04:00
end
describe '.find_by_ssh_key_id' do
context 'using an existing SSH key ID' do
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
it 'returns the corresponding User' do
expect(described_class.find_by_ssh_key_id(key.id)).to eq(user)
end
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
describe '.by_login' do
let(:username) { 'John' }
let!(:user) { create(:user, username: username) }
it 'gets the correct user' do
expect(User.by_login(user.email.upcase)).to eq user
expect(User.by_login(user.email)).to eq user
expect(User.by_login(username.downcase)).to eq user
expect(User.by_login(username)).to eq user
expect(User.by_login(nil)).to be_nil
expect(User.by_login('')).to be_nil
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
2015-07-23 17:16:39 -04:00
describe '.find_by_username!' do
it 'raises RecordNotFound' do
2017-06-21 09:48:12 -04:00
expect { described_class.find_by_username!('JohnDoe') }
.to raise_error(ActiveRecord::RecordNotFound)
2015-07-23 17:16:39 -04:00
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) }
context 'with a route matching the given path' do
let!(:route) { user.namespace.route }
it 'returns the user' do
expect(User.find_by_full_path(route.path)).to eq(user)
end
it 'is case-insensitive' do
expect(User.find_by_full_path(route.path.upcase)).to eq(user)
expect(User.find_by_full_path(route.path.downcase)).to eq(user)
end
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(User.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(User.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(user)
end
it 'is case-insensitive' do
expect(User.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(user)
expect(User.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(User.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(User.find_by_full_path('unknown', follow_redirects: true)).to eq(nil)
end
end
end
context 'with a group route matching the given path' do
context 'when the group namespace has an owner_id (legacy data)' do
let!(:group) { create(:group, path: 'group_path', owner: user) }
it 'returns nil' do
expect(User.find_by_full_path('group_path')).to eq(nil)
end
end
context 'when the group namespace does not have an owner_id' do
let!(:group) { create(:group, path: 'group_path') }
it 'returns nil' do
expect(User.find_by_full_path('group_path')).to eq(nil)
end
end
end
end
2014-02-06 04:12:59 -05:00
describe 'all_ssh_keys' do
it { is_expected.to have_many(:keys).dependent(:destroy) }
2014-02-06 04:12:59 -05:00
it "has all ssh keys" do
2014-02-06 04:12:59 -05:00
user = create :user
key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id
expect(user.all_ssh_keys).to include(a_string_starting_with(key.key))
2014-02-06 04:12:59 -05:00
end
2014-02-06 09:17:21 -05:00
end
2016-07-11 18:12:31 -04:00
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')
expect(user.avatar_type).to eq(['only images allowed'])
end
end
2014-01-18 14:07:00 -05:00
describe '#avatar_url' do
let(:user) { create(:user, :with_avatar) }
context 'when avatar file is uploaded' do
let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
let(:avatar_path) { "/uploads/system/user/avatar/#{user.id}/dk.png" }
it 'shows correct avatar url' do
expect(user.avatar_url).to eq(avatar_path)
expect(user.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
expect(user.avatar_url).to eq([gitlab_host, avatar_path].join)
end
end
end
2016-07-11 18:12:31 -04:00
describe '#requires_ldap_check?' do
let(:user) { User.new }
it 'is false when LDAP is disabled' do
# Create a condition which would otherwise cause 'true' to be returned
2015-05-21 17:49:06 -04:00
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
2015-05-21 17:49:06 -04:00
before do
allow(Gitlab.config.ldap).to receive(:enabled).and_return(true)
end
it 'is false for non-LDAP users' do
2015-05-21 17:49:06 -04:00
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
2015-05-21 17:49:06 -04:00
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
2016-07-11 18:12:31 -04:00
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
2016-07-11 18:12:31 -04:00
describe '#ldap_identity' do
it 'returns ldap identity' do
user = create :omniauth_user
expect(user.ldap_identity.provider).not_to be_empty
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
end
end
2014-01-18 14:07:00 -05:00
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'
2014-01-18 14:07:00 -05:00
expect(user.short_website_url).to eq 'test.com'
end
2014-02-06 04:12:59 -05:00
end
2014-06-26 03:49:14 -04:00
describe '#starred?' do
it 'determines if user starred a project' do
user = create :user
project1 = create(:empty_project, :public)
project2 = create(:empty_project, :public)
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_falsey
star1 = UsersStarProject.create!(project: project1, user: user)
expect(user.starred?(project1)).to be_truthy
expect(user.starred?(project2)).to be_falsey
star2 = UsersStarProject.create!(project: project2, user: user)
expect(user.starred?(project1)).to be_truthy
expect(user.starred?(project2)).to be_truthy
star1.destroy
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_truthy
star2.destroy
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_falsey
end
end
describe '#toggle_star' do
it 'toggles stars' do
2014-06-26 03:49:14 -04:00
user = create :user
project = create(:empty_project, :public)
2014-06-26 03:49:14 -04:00
expect(user.starred?(project)).to be_falsey
2014-06-26 03:49:14 -04:00
user.toggle_star(project)
expect(user.starred?(project)).to be_truthy
2014-06-26 03:49:14 -04:00
user.toggle_star(project)
expect(user.starred?(project)).to be_falsey
2014-06-26 03:49:14 -04:00
end
end
2014-10-10 08:15:34 -04:00
describe '#sort' do
2014-10-10 08:15:34 -04:00
before do
User.delete_all
@user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha'
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
@user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta'
2014-10-10 08:15:34 -04:00
end
2015-02-05 23:21:21 -05:00
context 'when sort by recent_sign_in' do
it 'sorts users by the recent sign-in time' do
expect(User.sort('recent_sign_in').first).to eq(@user)
end
it 'pushes users who never signed in to the end' do
expect(User.sort('recent_sign_in').third).to eq(@user2)
end
2014-10-10 08:15:34 -04:00
end
context 'when sort by oldest_sign_in' do
it 'sorts users by the oldest sign-in time' do
expect(User.sort('oldest_sign_in').first).to eq(@user1)
end
it 'pushes users who never signed in to the end' do
expect(User.sort('oldest_sign_in').third).to eq(@user2)
end
2014-10-10 08:15:34 -04:00
end
it 'sorts users in descending order by their creation time' do
expect(User.sort('created_desc').first).to eq(@user)
2014-10-10 08:15:34 -04:00
end
it 'sorts users in ascending order by their creation time' do
expect(User.sort('created_asc').first).to eq(@user2)
2014-10-10 08:15:34 -04:00
end
it 'sorts users by id in descending order when nil is passed' do
expect(User.sort(nil).first).to eq(@user2)
2014-10-10 08:15:34 -04:00
end
end
describe "#contributed_projects" do
subject { create(:user) }
let!(:project1) { create(:empty_project) }
let!(:project2) { create(:empty_project, forked_from_project: project3) }
let!(:project3) { create(:empty_project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) }
let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
project1.team << [subject, :master]
project2.team << [subject, :master]
end
it "includes IDs for projects the user has pushed to" do
expect(subject.contributed_projects).to include(project1)
end
it "includes IDs for projects the user has had merge requests merged into" do
expect(subject.contributed_projects).to include(project3)
end
it "doesn't include IDs for unrelated projects" do
expect(subject.contributed_projects).not_to include(project2)
end
end
2016-07-11 18:12:31 -04:00
describe '#can_be_removed?' do
subject { create(:user) }
context 'no owned groups' do
it { expect(subject.can_be_removed?).to be_truthy }
end
context 'has owned groups' do
before do
group = create(:group)
group.add_owner(subject)
end
it { expect(subject.can_be_removed?).to be_falsey }
end
end
describe "#recent_push" do
subject { create(:user) }
let!(:project1) { create(:project, :repository) }
let!(:project2) { create(:project, :repository, forked_from_project: project1) }
let!(:push_data) do
Gitlab::DataBuilder::Push.build_sample(project2, subject)
end
let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }
before do
project1.team << [subject, :master]
project2.team << [subject, :master]
end
it "includes push event" do
expect(subject.recent_push).to eq(push_event)
end
it "excludes push event if branch has been deleted" do
allow_any_instance_of(Repository).to receive(:branch_names).and_return(['foo'])
expect(subject.recent_push).to eq(nil)
end
it "excludes push event if MR is opened for it" do
create(:merge_request, source_project: project2, target_project: project1, source_branch: project2.default_branch, target_branch: 'fix', author: subject)
expect(subject.recent_push).to eq(nil)
end
it "includes push events on any of the provided projects" do
expect(subject.recent_push(project1)).to eq(nil)
expect(subject.recent_push(project2)).to eq(push_event)
push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)
expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
end
end
describe '#authorized_groups' do
let!(:user) { create(:user) }
let!(:private_group) { create(:group) }
before do
private_group.add_user(user, Gitlab::Access::MASTER)
end
subject { user.authorized_groups }
it { is_expected.to eq([private_group]) }
end
describe '#authorized_projects', truncate: true do
context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do
user = create(:user)
project = create(:empty_project, :private, namespace: user.namespace)
expect(user.authorized_projects(Gitlab::Access::REPORTER))
.to contain_exactly(project)
end
it 'includes projects for which the user is a master' do
user = create(:user)
project = create(:empty_project, :private)
project.team << [user, Gitlab::Access::MASTER]
expect(user.authorized_projects(Gitlab::Access::REPORTER))
.to contain_exactly(project)
end
end
it "includes user's personal projects" do
user = create(:user)
project = create(:empty_project, :private, namespace: user.namespace)
expect(user.authorized_projects).to include(project)
end
it "includes personal projects user has been given access to" do
user1 = create(:user)
user2 = create(:user)
project = create(:empty_project, :private, namespace: user1.namespace)
project.team << [user2, Gitlab::Access::DEVELOPER]
expect(user2.authorized_projects).to include(project)
end
it "includes projects of groups user has been added to" do
group = create(:group)
project = create(:empty_project, group: group)
user = create(:user)
group.add_developer(user)
expect(user.authorized_projects).to include(project)
end
it "does not include projects of groups user has been removed from" do
group = create(:group)
project = create(:empty_project, group: group)
user = create(:user)
member = group.add_developer(user)
expect(user.authorized_projects).to include(project)
member.destroy
expect(user.authorized_projects).not_to include(project)
end
it "includes projects shared with user's group" do
user = create(:user)
project = create(:empty_project, :private)
group = create(:group)
group.add_reporter(user)
project.project_group_links.create(group: group)
expect(user.authorized_projects).to include(project)
end
it "does not include destroyed projects user had access to" do
user1 = create(:user)
user2 = create(:user)
project = create(:empty_project, :private, namespace: user1.namespace)
project.team << [user2, Gitlab::Access::DEVELOPER]
expect(user2.authorized_projects).to include(project)
project.destroy
expect(user2.authorized_projects).not_to include(project)
end
it "does not include projects of destroyed groups user had access to" do
group = create(:group)
project = create(:empty_project, namespace: group)
user = create(:user)
group.add_developer(user)
expect(user.authorized_projects).to include(project)
group.destroy
expect(user.authorized_projects).not_to include(project)
end
end
describe '#projects_where_can_admin_issues' do
let(:user) { create(:user) }
it 'includes projects for which the user access level is above or equal to reporter' do
reporter_project = create(:empty_project) { |p| p.add_reporter(user) }
developer_project = create(:empty_project) { |p| p.add_developer(user) }
master_project = create(:empty_project) { |p| p.add_master(user) }
expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project])
expect(user.can?(:admin_issue, master_project)).to eq(true)
expect(user.can?(:admin_issue, developer_project)).to eq(true)
expect(user.can?(:admin_issue, reporter_project)).to eq(true)
end
it 'does not include for which the user access level is below reporter' do
project = create(:empty_project)
guest_project = create(:empty_project) { |p| p.add_guest(user) }
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, guest_project)).to eq(false)
expect(user.can?(:admin_issue, project)).to eq(false)
end
it 'does not include archived projects' do
project = create(:empty_project, :archived)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
end
it 'does not include projects for which issues are disabled' do
project = create(:empty_project, :issues_disabled)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
end
end
describe '#ci_authorized_runners' do
let(:user) { create(:user) }
let(:runner) { create(:ci_runner) }
before do
project.runners << runner
end
context 'without any projects' do
let(:project) { create(:empty_project) }
it 'does not load' do
expect(user.ci_authorized_runners).to be_empty
end
end
context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) }
let(:project) { create(:empty_project, namespace: namespace) }
it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner)
end
end
shared_examples :member do
context 'when the user is a master' do
before do
add_user(Gitlab::Access::MASTER)
end
it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner)
end
end
context 'when the user is a developer' do
before do
add_user(Gitlab::Access::DEVELOPER)
end
it 'does not load' do
expect(user.ci_authorized_runners).to be_empty
end
end
end
context 'with groups projects runners' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
2016-06-02 01:27:44 -04:00
def add_user(access)
group.add_user(user, access)
end
it_behaves_like :member
end
context 'with other projects runners' do
let(:project) { create(:empty_project) }
2016-06-02 01:27:44 -04:00
def add_user(access)
project.team << [user, access]
end
it_behaves_like :member
end
end
describe '#projects_with_reporter_access_limited_to' do
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
let(:user) { create(:user) }
before do
project1.team << [user, :reporter]
project2.team << [user, :guest]
end
it 'returns the projects when using a single project ID' do
projects = user.projects_with_reporter_access_limited_to(project1.id)
expect(projects).to eq([project1])
end
it 'returns the projects when using an Array of project IDs' do
projects = user.projects_with_reporter_access_limited_to([project1.id])
expect(projects).to eq([project1])
end
it 'returns the projects when using an ActiveRecord relation' do
2017-06-21 09:48:12 -04:00
projects = user
.projects_with_reporter_access_limited_to(Project.select(:id))
expect(projects).to eq([project1])
end
it 'does not return projects you do not have reporter access to' do
projects = user.projects_with_reporter_access_limited_to(project2.id)
expect(projects).to be_empty
end
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
describe '#all_expanded_groups' do
# foo/bar would also match foo/barbaz instead of just foo/bar and foo/bar/baz
let!(:user) { create(:user) }
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
# group
# _______ (foo) _______
# | |
# | |
# nested_group_1 nested_group_2
# (bar) (barbaz)
# | |
# | |
# nested_group_1_1 nested_group_2_1
# (baz) (baz)
#
let!(:group) { create :group }
let!(:nested_group_1) { create :group, parent: group, name: 'bar' }
let!(:nested_group_1_1) { create :group, parent: nested_group_1, name: 'baz' }
let!(:nested_group_2) { create :group, parent: group, name: 'barbaz' }
let!(:nested_group_2_1) { create :group, parent: nested_group_2, name: 'baz' }
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
subject { user.all_expanded_groups }
context 'user is not a member of any group' do
it 'returns an empty array' do
is_expected.to eq([])
end
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
context 'user is member of all groups' do
before do
group.add_owner(user)
nested_group_1.add_owner(user)
nested_group_1_1.add_owner(user)
nested_group_2.add_owner(user)
nested_group_2_1.add_owner(user)
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
it 'returns all groups' do
is_expected.to match_array [
group,
nested_group_1, nested_group_1_1,
nested_group_2, nested_group_2_1
]
end
end
2017-03-09 14:38:13 -05:00
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
context 'user is member of the top group' do
before do
group.add_owner(user)
end
2017-03-09 14:38:13 -05:00
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
if Group.supports_nested_groups?
it 'returns all groups' do
is_expected.to match_array [
group,
nested_group_1, nested_group_1_1,
nested_group_2, nested_group_2_1
]
end
else
it 'returns the top-level groups' do
is_expected.to match_array [group]
end
end
end
2017-03-09 14:38:13 -05:00
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
context 'user is member of the first child (internal node), branch 1', :nested_groups do
before do
nested_group_1.add_owner(user)
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
it 'returns the groups in the hierarchy' do
is_expected.to match_array [
group,
nested_group_1, nested_group_1_1
]
end
end
context 'user is member of the first child (internal node), branch 2', :nested_groups do
before do
nested_group_2.add_owner(user)
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
it 'returns the groups in the hierarchy' do
is_expected.to match_array [
group,
nested_group_2, nested_group_2_1
]
end
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
context 'user is member of the last child (leaf node)', :nested_groups do
before do
nested_group_1_1.add_owner(user)
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
it 'returns the groups in the hierarchy' do
is_expected.to match_array [
group,
nested_group_1, nested_group_1_1
]
end
end
end
describe '#refresh_authorized_projects', redis: true do
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
let(:user) { create(:user) }
before do
project1.team << [user, :reporter]
project2.team << [user, :guest]
user.project_authorizations.delete_all
user.refresh_authorized_projects
end
it 'refreshes the list of authorized projects' do
expect(user.project_authorizations.count).to eq(2)
end
it 'stores the correct access levels' do
expect(user.project_authorizations.where(access_level: Gitlab::Access::GUEST).exists?).to eq(true)
expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true)
end
end
2017-02-06 18:19:37 -05:00
describe '#access_level=' do
let(:user) { build(:user) }
it 'does nothing for an invalid access level' do
user.access_level = :invalid_access_level
expect(user.access_level).to eq(:regular)
expect(user.admin).to be false
end
it "assigns the 'admin' access level" do
user.access_level = :admin
expect(user.access_level).to eq(:admin)
expect(user.admin).to be true
end
it "doesn't clear existing access levels when an invalid access level is passed in" do
user.access_level = :admin
user.access_level = :invalid_access_level
expect(user.access_level).to eq(:admin)
expect(user.admin).to be true
end
it "accepts string values in addition to symbols" do
user.access_level = 'admin'
expect(user.access_level).to eq(:admin)
expect(user.admin).to be true
end
end
describe '#full_private_access?' do
it 'returns false for regular user' do
user = build(:user)
expect(user.full_private_access?).to be_falsy
end
it 'returns true for admin user' do
user = build(:user, :admin)
expect(user.full_private_access?).to be_truthy
end
end
describe '.ghost' do
it "creates a ghost user if one isn't already present" do
ghost = User.ghost
expect(ghost).to be_ghost
expect(ghost).to be_persisted
end
it "does not create a second ghost user if one is already present" do
expect do
User.ghost
User.ghost
end.to change { User.count }.by(1)
expect(User.ghost).to eq(User.ghost)
end
context "when a regular user exists with the username 'ghost'" do
it "creates a ghost user with a non-conflicting username" do
create(:user, username: 'ghost')
ghost = User.ghost
expect(ghost).to be_persisted
expect(ghost.username).to eq('ghost1')
end
end
context "when a regular user exists with the email 'ghost@example.com'" do
it "creates a ghost user with a non-conflicting email" do
create(:user, email: 'ghost@example.com')
ghost = User.ghost
expect(ghost).to be_persisted
expect(ghost.email).to eq('ghost1@example.com')
end
end
context 'when a domain whitelist is in place' do
before do
stub_application_setting(domain_whitelist: ['gitlab.com'])
end
it 'creates a ghost user' do
expect(User.ghost).to be_persisted
end
end
end
2017-01-24 16:09:58 -05:00
describe '#update_two_factor_requirement' do
let(:user) { create :user }
context 'with 2FA requirement on groups' do
let(:group1) { create :group, require_two_factor_authentication: true, two_factor_grace_period: 23 }
let(:group2) { create :group, require_two_factor_authentication: true, two_factor_grace_period: 32 }
before do
group1.add_user(user, GroupMember::OWNER)
group2.add_user(user, GroupMember::OWNER)
user.update_two_factor_requirement
end
it 'requires 2FA' do
expect(user.require_two_factor_authentication_from_group).to be true
2017-01-24 16:09:58 -05:00
end
it 'uses the shortest grace period' do
expect(user.two_factor_grace_period).to be 23
end
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
context 'with 2FA requirement on nested parent group', :nested_groups do
2017-03-09 14:38:13 -05:00
let!(:group1) { create :group, require_two_factor_authentication: true }
let!(:group1a) { create :group, require_two_factor_authentication: false, parent: group1 }
before do
group1a.add_user(user, GroupMember::OWNER)
user.update_two_factor_requirement
end
it 'requires 2FA' do
expect(user.require_two_factor_authentication_from_group).to be true
2017-03-09 14:38:13 -05:00
end
end
Use CTEs for nested groups and authorizations This commit introduces the usage of Common Table Expressions (CTEs) to efficiently retrieve nested group hierarchies, without having to rely on the "routes" table (which is an _incredibly_ inefficient way of getting the data). This requires a patch to ActiveRecord (found in the added initializer) to work properly as ActiveRecord doesn't support WITH statements properly out of the box. Unfortunately MySQL provides no efficient way of getting nested groups. For example, the old routes setup could easily take 5-10 seconds depending on the amount of "routes" in a database. Providing vastly different logic for both MySQL and PostgreSQL will negatively impact the development process. Because of this the various nested groups related methods return empty relations when used in combination with MySQL. For project authorizations the logic is split up into two classes: * Gitlab::ProjectAuthorizations::WithNestedGroups * Gitlab::ProjectAuthorizations::WithoutNestedGroups Both classes get the fresh project authorizations (= as they should be in the "project_authorizations" table), including nested groups if PostgreSQL is used. The logic of these two classes is quite different apart from their public interface. This complicates development a bit, but unfortunately there is no way around this. This commit also introduces Gitlab::GroupHierarchy. This class can be used to get the ancestors and descendants of a base relation, or both by using a UNION. This in turn is used by methods such as: * Namespace#ancestors * Namespace#descendants * User#all_expanded_groups Again this class relies on CTEs and thus only works on PostgreSQL. The Namespace methods will return an empty relation when MySQL is used, while User#all_expanded_groups will return only the groups a user is a direct member of. Performance wise the impact is quite large. For example, on GitLab.com Namespace#descendants used to take around 580 ms to retrieve data for a particular user. Using CTEs we are able to reduce this down to roughly 1 millisecond, returning the exact same data. == On The Fly Refreshing Refreshing of authorizations on the fly (= when users.authorized_projects_populated was not set) is removed with this commit. This simplifies the code, and ensures any queries used for authorizations are not mutated because they are executed in a Rails scope (e.g. Project.visible_to_user). This commit includes a migration to schedule refreshing authorizations for all users, ensuring all of them have their authorizations in place. Said migration schedules users in batches of 5000, with 5 minutes between every batch to smear the load around a bit. == Spec Changes This commit also introduces some changes to various specs. For example, some specs for ProjectTeam assumed that creating a personal project would _not_ lead to the owner having access, which is incorrect. Because we also no longer refresh authorizations on the fly for new users some code had to be added to the "empty_project" factory. This chunk of code ensures that the owner's permissions are refreshed after creating the project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
context 'with 2FA requirement on nested child group', :nested_groups do
2017-03-09 14:38:13 -05:00
let!(:group1) { create :group, require_two_factor_authentication: false }
let!(:group1a) { create :group, require_two_factor_authentication: true, parent: group1 }
before do
group1.add_user(user, GroupMember::OWNER)
user.update_two_factor_requirement
end
it 'requires 2FA' do
expect(user.require_two_factor_authentication_from_group).to be true
2017-03-09 14:38:13 -05:00
end
end
2017-01-24 16:09:58 -05:00
context 'without 2FA requirement on groups' do
let(:group) { create :group }
before do
group.add_user(user, GroupMember::OWNER)
user.update_two_factor_requirement
end
it 'does not require 2FA' do
expect(user.require_two_factor_authentication_from_group).to be false
2017-01-24 16:09:58 -05:00
end
it 'falls back to the default grace period' do
expect(user.two_factor_grace_period).to be 48
end
end
end
2017-04-19 06:11:04 -04:00
context '.active' do
before do
User.ghost
create(:user, name: 'user', state: 'active')
create(:user, name: 'user', state: 'blocked')
end
it 'only counts active and non internal users' do
expect(User.active.count).to eq(1)
end
end
describe 'preferred language' do
it 'is English by default' do
user = create(:user)
expect(user.preferred_language).to eq('en')
end
end
context '#invalidate_issue_cache_counts' do
let(:user) { build_stubbed(:user) }
it 'invalidates cache for issue counter' do
cache_mock = double
expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
allow(Rails).to receive(:cache).and_return(cache_mock)
user.invalidate_issue_cache_counts
end
end
context '#invalidate_merge_request_cache_counts' do
let(:user) { build_stubbed(:user) }
it 'invalidates cache for Merge Request counter' do
cache_mock = double
expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_merge_requests_count'])
allow(Rails).to receive(:cache).and_return(cache_mock)
user.invalidate_merge_request_cache_counts
end
end
2011-10-08 17:36:38 -04:00
end