2011-10-08 17:36:38 -04:00
|
|
|
require 'spec_helper'
|
|
|
|
|
2015-12-09 04:50:51 -05:00
|
|
|
describe User, models: true do
|
2015-05-02 09:53:32 -04:00
|
|
|
include Gitlab::CurrentSettings
|
|
|
|
|
2015-05-02 23:11:21 -04:00
|
|
|
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
|
|
|
|
|
2017-06-06 09:42:45 -04:00
|
|
|
describe 'delegations' do
|
|
|
|
it { is_expected.to delegate_method(:path).to(:namespace).with_prefix }
|
|
|
|
end
|
|
|
|
|
2015-05-02 23:11:21 -04:00
|
|
|
describe 'associations' do
|
2015-02-12 13:17:35 -05:00
|
|
|
it { is_expected.to have_one(:namespace) }
|
2016-10-18 17:20:36 -04:00
|
|
|
it { is_expected.to have_many(:snippets).dependent(:destroy) }
|
2015-02-12 13:17:35 -05:00
|
|
|
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) }
|
2017-02-07 07:56:50 -05:00
|
|
|
it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
|
2016-10-18 17:20:36 -04:00
|
|
|
it { is_expected.to have_many(:events).dependent(:destroy) }
|
2015-02-12 13:17:35 -05:00
|
|
|
it { is_expected.to have_many(:recent_events).class_name('Event') }
|
2017-06-01 12:30:01 -04:00
|
|
|
it { is_expected.to have_many(:issues).dependent(:destroy) }
|
2015-02-12 13:17:35 -05:00
|
|
|
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) }
|
2016-04-25 14:10:20 -04:00
|
|
|
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) }
|
2017-03-02 12:02:33 -05:00
|
|
|
it { is_expected.to have_many(:uploads).dependent(:destroy) }
|
2017-04-06 13:03:07 -04:00
|
|
|
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
|
2016-06-24 06:01:48 -04:00
|
|
|
|
2017-04-05 12:43:39 -04:00
|
|
|
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
|
2016-06-24 06:01:48 -04:00
|
|
|
|
|
|
|
describe '#group_members' do
|
|
|
|
it 'does not include group memberships for which user is a requester' do
|
|
|
|
user = create(:user)
|
2016-11-11 07:51:50 -05:00
|
|
|
group = create(:group, :public, :access_requestable)
|
2016-06-24 06:01:48 -04:00
|
|
|
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)
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, :public, :access_requestable)
|
2016-06-24 06:01:48 -04:00
|
|
|
project.request_access(user)
|
|
|
|
|
|
|
|
expect(user.project_members).to be_empty
|
|
|
|
end
|
|
|
|
end
|
2012-08-29 11:36:02 -04:00
|
|
|
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
|
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
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
|
2016-01-02 19:53:45 -05:00
|
|
|
expect(subject).to validate_uniqueness_of(:username).case_insensitive
|
2015-12-07 16:17:12 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-02-12 13:17:35 -05:00
|
|
|
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) }
|
2017-03-24 07:32:03 -04:00
|
|
|
it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) }
|
2012-08-29 11:36:02 -04:00
|
|
|
|
2016-12-02 07:54:57 -05:00
|
|
|
it { is_expected.to validate_length_of(:bio).is_at_most(255) }
|
2014-01-16 06:14:47 -05:00
|
|
|
|
2016-02-09 09:51:06 -05:00
|
|
|
it_behaves_like 'an object with email-formated attributes', :email do
|
|
|
|
subject { build(:user) }
|
|
|
|
end
|
2014-06-11 07:22:56 -04:00
|
|
|
|
2016-02-09 09:51:06 -05: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
|
2015-05-02 09:53:32 -04:00
|
|
|
|
2016-02-09 09:51:06 -05:00
|
|
|
describe 'email' do
|
2016-07-15 14:20:45 -04:00
|
|
|
context 'when no signup domains whitelisted' do
|
2016-06-02 11:30:27 -04:00
|
|
|
before do
|
2016-07-15 19:30:38 -04:00
|
|
|
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return([])
|
2016-06-02 11:30:27 -04:00
|
|
|
end
|
2016-06-07 08:32:48 -04:00
|
|
|
|
2015-05-02 09:53:32 -04:00
|
|
|
it 'accepts any email' do
|
|
|
|
user = build(:user, email: "info@example.com")
|
|
|
|
expect(user).to be_valid
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-15 14:20:45 -04:00
|
|
|
context 'when a signup domain is whitelisted and subdomains are allowed' do
|
2016-06-02 11:30:27 -04:00
|
|
|
before do
|
2016-07-15 19:30:38 -04:00
|
|
|
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com', '*.example.com'])
|
2016-06-02 11:30:27 -04:00
|
|
|
end
|
2016-06-07 08:32:48 -04:00
|
|
|
|
2015-05-02 09:53:32 -04:00
|
|
|
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
|
|
|
|
|
2016-07-15 14:20:45 -04:00
|
|
|
context 'when a signup domain is whitelisted and subdomains are not allowed' do
|
2016-06-02 11:30:27 -04:00
|
|
|
before do
|
2016-07-15 19:30:38 -04:00
|
|
|
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com'])
|
2016-06-02 11:30:27 -04:00
|
|
|
end
|
2015-05-02 09:53:32 -04:00
|
|
|
|
|
|
|
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
|
2017-01-12 12:03:25 -05:00
|
|
|
|
|
|
|
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
|
2015-05-02 09:53:32 -04:00
|
|
|
end
|
2016-03-31 13:59:19 -04:00
|
|
|
|
2016-07-14 14:19:40 -04:00
|
|
|
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
|
|
|
|
|
2016-07-18 18:49:33 -04:00
|
|
|
context 'when a signup domain is blacklisted' do
|
2016-07-14 14:19:40 -04:00
|
|
|
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
|
2017-01-12 12:03:25 -05:00
|
|
|
|
|
|
|
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
|
2016-07-14 14:19:40 -04:00
|
|
|
end
|
|
|
|
|
2016-07-18 18:49:33 -04:00
|
|
|
context 'when a signup domain is blacklisted but a wildcard subdomain is allowed' do
|
2016-07-14 14:19:40 -04:00
|
|
|
before do
|
|
|
|
allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['test.example.com'])
|
2016-07-15 19:30:38 -04:00
|
|
|
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['*.example.com'])
|
2016-07-14 14:19:40 -04:00
|
|
|
end
|
|
|
|
|
2016-08-01 11:00:44 -04:00
|
|
|
it 'gives priority to whitelist and allow info@test.example.com' do
|
2016-07-14 14:19:40 -04:00
|
|
|
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
|
2016-07-15 19:30:38 -04:00
|
|
|
allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['test.com'])
|
2016-07-14 14:19:40 -04:00
|
|
|
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
|
|
|
|
|
2016-03-31 13:59:19 -04:00
|
|
|
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
|
2014-01-16 06:14:47 -05:00
|
|
|
end
|
2016-06-06 00:38:42 -04:00
|
|
|
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?) }
|
2015-02-12 13:17:35 -05:00
|
|
|
it { is_expected.to respond_to(:name) }
|
|
|
|
it { is_expected.to respond_to(:private_token) }
|
2016-03-10 16:08:11 -05:00
|
|
|
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
|
|
|
|
|
2017-05-02 16:52:14 -04:00
|
|
|
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)
|
2017-05-05 03:29:03 -04:00
|
|
|
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 }
|
2017-05-02 16:52:14 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-07 07:56:50 -05:00
|
|
|
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
|
|
|
|
|
2015-07-29 10:32:01 -04:00
|
|
|
describe '#confirm' do
|
2016-06-02 11:30:27 -04:00
|
|
|
before do
|
|
|
|
allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
|
|
|
|
end
|
2016-06-07 08:32:48 -04:00
|
|
|
|
2015-07-29 10:32:01 -04:00
|
|
|
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
|
2015-09-19 21:15:13 -04:00
|
|
|
user.confirm
|
2015-07-29 10:32:01 -04:00
|
|
|
expect(user.confirmed?).to be_truthy
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-05-02 23:11:21 -04:00
|
|
|
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
|
|
|
|
|
2012-09-04 00:04:36 -04:00
|
|
|
describe '#generate_password' do
|
2016-08-01 11:00:44 -04:00
|
|
|
it "does not generate password by default" do
|
2013-11-25 13:07:55 -05:00
|
|
|
user = create(:user, password: 'abcdefghe')
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.password).to eq('abcdefghe')
|
2012-09-04 00:04:36 -04:00
|
|
|
end
|
2012-06-24 16:51:58 -04:00
|
|
|
end
|
|
|
|
|
2012-09-04 00:04:36 -04:00
|
|
|
describe 'authentication token' do
|
2016-08-01 11:00:44 -04:00
|
|
|
it "has authentication token" do
|
2012-11-05 22:31:55 -05:00
|
|
|
user = create(:user)
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.authentication_token).not_to be_blank
|
2012-09-04 00:04:36 -04:00
|
|
|
end
|
2011-11-15 02:08:05 -05:00
|
|
|
end
|
2013-01-02 12:00:00 -05:00
|
|
|
|
2017-05-23 08:11:24 -04:00
|
|
|
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
|
|
|
|
|
2017-06-16 07:28:49 -04:00
|
|
|
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)
|
|
|
|
|
2017-06-16 07:28:49 -04:00
|
|
|
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
|
2017-05-23 16:47:05 -04:00
|
|
|
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
|
|
|
|
|
2015-10-02 00:14:47 -04:00
|
|
|
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
|
|
|
|
|
2015-07-10 16:08:39 -04:00
|
|
|
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
|
2016-02-29 13:56:40 -05:00
|
|
|
expect(user.otp_grace_period_started_at).not_to be_nil
|
2015-07-10 16:08:39 -04:00
|
|
|
|
|
|
|
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
|
2016-02-29 13:56:40 -05:00
|
|
|
expect(user.otp_grace_period_started_at).to be_nil
|
2015-07-10 16:08:39 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-01-02 12:00:00 -05:00
|
|
|
describe 'projects' do
|
|
|
|
before do
|
2017-01-19 16:44:25 -05:00
|
|
|
@user = create(:user)
|
2013-06-04 10:50:51 -04:00
|
|
|
|
2017-01-19 16:44:25 -05:00
|
|
|
@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
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
2015-02-12 13:17:35 -05:00
|
|
|
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) }
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
describe 'groups' do
|
|
|
|
before do
|
|
|
|
@user = create :user
|
2013-09-26 07:52:17 -04:00
|
|
|
@group = create :group
|
|
|
|
@group.add_owner(@user)
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
2015-02-12 13:17:35 -05:00
|
|
|
it { expect(@user.several_namespaces?).to be_truthy }
|
|
|
|
it { expect(@user.authorized_groups).to eq([@group]) }
|
|
|
|
it { expect(@user.owned_groups).to eq([@group]) }
|
2015-05-25 16:51:37 -04:00
|
|
|
it { expect(@user.namespaces).to match_array([@user.namespace, @group]) }
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
2013-09-11 08:10:45 -04:00
|
|
|
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)
|
2013-09-11 08:10:45 -04:00
|
|
|
|
2014-09-14 12:32:51 -04:00
|
|
|
@group.add_user(@user2, GroupMember::OWNER)
|
2013-09-11 08:10:45 -04:00
|
|
|
end
|
|
|
|
|
2015-02-12 13:17:35 -05:00
|
|
|
it { expect(@user2.several_namespaces?).to be_truthy }
|
2013-09-11 08:10:45 -04:00
|
|
|
end
|
|
|
|
|
2013-01-02 12:00:00 -05:00
|
|
|
describe 'namespaced' do
|
|
|
|
before do
|
|
|
|
@user = create :user
|
2017-01-19 16:44:25 -05:00
|
|
|
@project = create(:empty_project, namespace: @user.namespace)
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
2015-02-12 13:17:35 -05:00
|
|
|
it { expect(@user.several_namespaces?).to be_falsey }
|
2015-05-25 16:51:37 -04:00
|
|
|
it { expect(@user.namespaces).to eq([@user.namespace]) }
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
describe 'blocking user' do
|
|
|
|
let(:user) { create(:user, name: 'John Smith') }
|
|
|
|
|
2016-08-01 11:00:44 -04:00
|
|
|
it "blocks user" do
|
2013-01-02 12:00:00 -05:00
|
|
|
user.block
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.blocked?).to be_truthy
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-06-19 16:31:36 -04:00
|
|
|
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
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
2015-06-19 16:31:36 -04:00
|
|
|
it 'filters by admins' do
|
|
|
|
expect(User).to receive(:admins).and_return([user])
|
|
|
|
|
|
|
|
expect(User.filter('admins')).to include user
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
2015-06-19 16:31:36 -04:00
|
|
|
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
|
2013-01-02 12:00:00 -05:00
|
|
|
end
|
|
|
|
|
2016-11-16 04:49:45 -05:00
|
|
|
describe '.without_projects' do
|
2016-11-16 14:19:22 -05:00
|
|
|
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
|
|
|
|
|
2013-08-15 17:43:46 -04:00
|
|
|
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 }
|
2015-02-12 13:17:35 -05:00
|
|
|
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') }
|
2016-07-06 13:46:41 -04:00
|
|
|
it { expect(user.external).to be_falsey }
|
2013-08-15 17:43:46 -04:00
|
|
|
end
|
2013-03-11 02:44:45 -04:00
|
|
|
|
2014-06-30 07:22:09 -04:00
|
|
|
describe 'with defaults' do
|
2013-08-15 17:43:46 -04:00
|
|
|
let(:user) { User.new }
|
2013-09-14 15:01:31 -04:00
|
|
|
|
2016-08-01 11:00:44 -04:00
|
|
|
it "applies defaults to user" do
|
2015-02-12 13:17:35 -05:00
|
|
|
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)
|
2016-03-10 16:08:11 -05:00
|
|
|
expect(user.external).to be_falsey
|
2013-08-15 17:43:46 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-06-30 07:22:09 -04:00
|
|
|
describe 'with default overrides' do
|
2017-02-13 12:50:13 -05:00
|
|
|
let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true) }
|
2013-09-14 15:01:31 -04:00
|
|
|
|
2016-08-01 11:00:44 -04:00
|
|
|
it "applies defaults to user" do
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.projects_limit).to eq(123)
|
|
|
|
expect(user.can_create_group).to be_falsey
|
2013-08-15 17:43:46 -04:00
|
|
|
end
|
2013-03-11 02:44:45 -04:00
|
|
|
end
|
2016-07-06 13:46:41 -04:00
|
|
|
|
|
|
|
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
|
2016-11-30 07:26:22 -05:00
|
|
|
|
2016-12-01 08:11:29 -05:00
|
|
|
describe '#require_ssh_key?' do
|
|
|
|
protocol_and_expectation = {
|
|
|
|
'http' => false,
|
|
|
|
'ssh' => true,
|
2017-05-03 07:22:03 -04:00
|
|
|
'' => true
|
2016-12-01 08:11:29 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2016-11-30 07:26:22 -05:00
|
|
|
end
|
|
|
|
end
|
2013-03-11 02:44:45 -04:00
|
|
|
end
|
2013-06-28 08:59:05 -04:00
|
|
|
|
2015-06-23 18:04:21 -04:00
|
|
|
describe '.find_by_any_email' do
|
2015-06-17 16:07:09 -04:00
|
|
|
it 'finds by primary email' do
|
|
|
|
user = create(:user, email: 'foo@example.com')
|
|
|
|
|
2015-06-23 18:04:21 -04:00
|
|
|
expect(User.find_by_any_email(user.email)).to eq user
|
2015-06-17 16:07:09 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'finds by secondary email' do
|
|
|
|
email = create(:email, email: 'foo@example.com')
|
|
|
|
user = email.user
|
|
|
|
|
2015-06-23 18:04:21 -04:00
|
|
|
expect(User.find_by_any_email(email.email)).to eq user
|
2015-06-17 16:07:09 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns nil when nothing found' do
|
2015-06-23 18:04:21 -04:00
|
|
|
expect(User.find_by_any_email('')).to be_nil
|
2015-06-17 16:07:09 -04:00
|
|
|
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
|
2016-10-25 13:14:58 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
describe '.search_with_secondary_emails' do
|
2017-02-22 12:51:46 -05:00
|
|
|
delegate :search_with_secondary_emails, to: :described_class
|
2016-10-25 13:14:58 -04:00
|
|
|
|
2017-03-13 06:05:15 -04:00
|
|
|
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' ) }
|
2017-03-17 05:54:56 -04:00
|
|
|
let!(:email) do
|
|
|
|
create(:email, user: another_user, email: 'alias@example.com')
|
|
|
|
end
|
2016-10-25 13:14:58 -04:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2016-10-04 12:03:10 -04:00
|
|
|
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
|
|
|
|
|
2014-10-22 11:29:26 -04:00
|
|
|
describe '.by_login' do
|
|
|
|
let(:username) { 'John' }
|
|
|
|
let!(:user) { create(:user, username: username) }
|
|
|
|
|
2016-08-01 11:00:44 -04:00
|
|
|
it 'gets the correct user' do
|
2014-10-22 11:29:26 -04:00
|
|
|
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
|
|
|
|
|
2016-11-14 09:55:31 -05:00
|
|
|
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
|
|
|
|
|
2017-05-01 16:46:30 -04:00
|
|
|
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
|
2017-05-17 20:01:19 -04:00
|
|
|
context 'when the group namespace has an owner_id (legacy data)' do
|
|
|
|
let!(:group) { create(:group, path: 'group_path', owner: user) }
|
2017-05-01 16:46:30 -04:00
|
|
|
|
2017-05-17 20:01:19 -04:00
|
|
|
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
|
2017-05-01 16:46:30 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-02-06 04:12:59 -05:00
|
|
|
describe 'all_ssh_keys' do
|
2015-02-12 13:17:35 -05:00
|
|
|
it { is_expected.to have_many(:keys).dependent(:destroy) }
|
2014-02-06 04:12:59 -05:00
|
|
|
|
2016-08-01 11:00:44 -04: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
|
|
|
|
|
2016-08-02 07:49:59 -04:00
|
|
|
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
|
2014-06-17 15:51:43 -04:00
|
|
|
|
2016-07-11 18:12:31 -04:00
|
|
|
describe '#avatar_type' do
|
2013-12-24 03:55:45 -05:00
|
|
|
let(:user) { create(:user) }
|
|
|
|
|
2017-01-19 06:10:13 -05:00
|
|
|
it 'is true if avatar is image' do
|
2013-12-24 03:55:45 -05:00
|
|
|
user.update_attribute(:avatar, 'uploads/avatar.png')
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.avatar_type).to be_truthy
|
2013-12-24 03:55:45 -05:00
|
|
|
end
|
|
|
|
|
2017-01-19 06:10:13 -05:00
|
|
|
it 'is false if avatar is html page' do
|
2013-12-24 03:55:45 -05:00
|
|
|
user.update_attribute(:avatar, 'uploads/avatar.html')
|
2017-01-19 06:10:13 -05:00
|
|
|
expect(user.avatar_type).to eq(['only images allowed'])
|
2013-12-24 03:55:45 -05:00
|
|
|
end
|
|
|
|
end
|
2014-01-18 14:07:00 -05:00
|
|
|
|
2017-05-03 11:25:31 -04:00
|
|
|
describe '#avatar_url' do
|
|
|
|
let(:user) { create(:user, :with_avatar) }
|
|
|
|
|
|
|
|
context 'when avatar file is uploaded' do
|
2017-05-10 00:26:17 -04:00
|
|
|
let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
|
2017-06-07 23:32:38 -04:00
|
|
|
let(:avatar_path) { "/uploads/system/user/avatar/#{user.id}/dk.png" }
|
2017-05-03 11:25:31 -04:00
|
|
|
|
2017-05-10 00:26:17 -04:00
|
|
|
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
|
2017-05-03 11:25:31 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-11 18:12:31 -04:00
|
|
|
describe '#requires_ldap_check?' do
|
2014-08-06 09:16:45 -04:00
|
|
|
let(:user) { User.new }
|
|
|
|
|
2014-08-06 09:17:12 -04:00
|
|
|
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)
|
2014-08-06 09:17:12 -04:00
|
|
|
user.last_credential_check_at = nil
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.requires_ldap_check?).to be_falsey
|
2014-08-06 09:16:45 -04:00
|
|
|
end
|
|
|
|
|
2014-08-06 09:17:12 -04:00
|
|
|
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
|
2014-08-06 09:16:45 -04:00
|
|
|
|
2014-08-06 09:17:12 -04:00
|
|
|
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)
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.requires_ldap_check?).to be_falsey
|
2014-08-06 09:16:45 -04:00
|
|
|
end
|
|
|
|
|
2014-08-06 09:17:12 -04:00
|
|
|
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
|
2014-08-06 09:17:12 -04:00
|
|
|
|
|
|
|
it 'is true when the user has never had an LDAP check before' do
|
|
|
|
user.last_credential_check_at = nil
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.requires_ldap_check?).to be_truthy
|
2014-08-06 09:17:12 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'is true when the last LDAP check happened over 1 hour ago' do
|
|
|
|
user.last_credential_check_at = 2.hours.ago
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.requires_ldap_check?).to be_truthy
|
2014-08-06 09:17:12 -04:00
|
|
|
end
|
2014-08-06 09:16:45 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-12-29 14:53:55 -05:00
|
|
|
context 'ldap synchronized user' do
|
2016-07-11 18:12:31 -04:00
|
|
|
describe '#ldap_user?' do
|
2015-12-29 14:53:55 -05:00
|
|
|
it 'is true if provider name starts with ldap' do
|
|
|
|
user = create(:omniauth_user, provider: 'ldapmain')
|
|
|
|
expect(user.ldap_user?).to be_truthy
|
|
|
|
end
|
2014-10-13 11:33:44 -04:00
|
|
|
|
2015-12-29 14:53:55 -05:00
|
|
|
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
|
2014-10-13 11:33:44 -04:00
|
|
|
end
|
|
|
|
|
2016-07-11 18:12:31 -04:00
|
|
|
describe '#ldap_identity' do
|
2015-12-29 14:53:55 -05:00
|
|
|
it 'returns ldap identity' do
|
|
|
|
user = create :omniauth_user
|
|
|
|
expect(user.ldap_identity.provider).not_to be_empty
|
|
|
|
end
|
2014-10-13 11:33:44 -04:00
|
|
|
end
|
|
|
|
|
2015-12-29 14:53:55 -05:00
|
|
|
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
|
2014-11-27 06:34:39 -05:00
|
|
|
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-02-11 10:42:27 -05:00
|
|
|
|
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
|
|
|
|
2017-01-19 06:10:13 -05:00
|
|
|
describe '#starred?' do
|
|
|
|
it 'determines if user starred a project' do
|
2014-07-14 09:17:59 -04:00
|
|
|
user = create :user
|
2017-01-19 16:44:25 -05:00
|
|
|
project1 = create(:empty_project, :public)
|
|
|
|
project2 = create(:empty_project, :public)
|
2014-07-14 09:17:59 -04:00
|
|
|
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.starred?(project1)).to be_falsey
|
|
|
|
expect(user.starred?(project2)).to be_falsey
|
2014-07-14 09:17:59 -04:00
|
|
|
|
|
|
|
star1 = UsersStarProject.create!(project: project1, user: user)
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.starred?(project1)).to be_truthy
|
|
|
|
expect(user.starred?(project2)).to be_falsey
|
2014-07-14 09:17:59 -04:00
|
|
|
|
|
|
|
star2 = UsersStarProject.create!(project: project2, user: user)
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.starred?(project1)).to be_truthy
|
|
|
|
expect(user.starred?(project2)).to be_truthy
|
2014-07-14 09:17:59 -04:00
|
|
|
|
|
|
|
star1.destroy
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.starred?(project1)).to be_falsey
|
|
|
|
expect(user.starred?(project2)).to be_truthy
|
2014-07-14 09:17:59 -04:00
|
|
|
|
|
|
|
star2.destroy
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.starred?(project1)).to be_falsey
|
|
|
|
expect(user.starred?(project2)).to be_falsey
|
2014-07-14 09:17:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-01-19 06:10:13 -05:00
|
|
|
describe '#toggle_star' do
|
|
|
|
it 'toggles stars' do
|
2014-06-26 03:49:14 -04:00
|
|
|
user = create :user
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, :public)
|
2014-06-26 03:49:14 -04:00
|
|
|
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.starred?(project)).to be_falsey
|
2014-06-26 03:49:14 -04:00
|
|
|
user.toggle_star(project)
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(user.starred?(project)).to be_truthy
|
2014-06-26 03:49:14 -04:00
|
|
|
user.toggle_star(project)
|
2015-02-12 13:17:35 -05:00
|
|
|
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
|
|
|
|
2017-01-19 06:10:13 -05: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'
|
2017-01-19 06:10:13 -05:00
|
|
|
@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
|
|
|
|
2017-01-19 06:10:13 -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
|
|
|
|
|
2017-01-19 06:10:13 -05:00
|
|
|
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
|
|
|
|
|
2017-01-19 06:10:13 -05:00
|
|
|
it 'sorts users in descending order by their creation time' do
|
2015-02-12 13:17:35 -05:00
|
|
|
expect(User.sort('created_desc').first).to eq(@user)
|
2014-10-10 08:15:34 -04:00
|
|
|
end
|
|
|
|
|
2017-01-19 06:10:13 -05:00
|
|
|
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
|
|
|
|
|
2017-01-19 06:10:13 -05:00
|
|
|
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
|
2015-02-27 04:47:37 -05:00
|
|
|
|
2015-11-16 08:28:02 -05:00
|
|
|
describe "#contributed_projects" do
|
2015-02-27 04:47:37 -05:00
|
|
|
subject { create(:user) }
|
2017-01-19 16:44:25 -05:00
|
|
|
let!(:project1) { create(:empty_project) }
|
|
|
|
let!(:project2) { create(:empty_project, forked_from_project: project3) }
|
|
|
|
let!(:project3) { create(:empty_project) }
|
2015-02-27 04:47:37 -05:00
|
|
|
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
|
2017-01-29 17:37:24 -05:00
|
|
|
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) }
|
2015-02-27 04:47:37 -05:00
|
|
|
|
|
|
|
before do
|
|
|
|
project1.team << [subject, :master]
|
|
|
|
project2.team << [subject, :master]
|
|
|
|
end
|
|
|
|
|
|
|
|
it "includes IDs for projects the user has pushed to" do
|
2015-11-16 08:28:02 -05:00
|
|
|
expect(subject.contributed_projects).to include(project1)
|
2015-02-27 04:47:37 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "includes IDs for projects the user has had merge requests merged into" do
|
2015-11-16 08:28:02 -05:00
|
|
|
expect(subject.contributed_projects).to include(project3)
|
2015-02-27 04:47:37 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't include IDs for unrelated projects" do
|
2015-11-16 08:28:02 -05:00
|
|
|
expect(subject.contributed_projects).not_to include(project2)
|
2015-02-27 04:47:37 -05:00
|
|
|
end
|
|
|
|
end
|
2015-05-28 06:00:02 -04:00
|
|
|
|
2016-07-11 18:12:31 -04:00
|
|
|
describe '#can_be_removed?' do
|
2015-05-28 06:00:02 -04:00
|
|
|
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
|
2015-08-16 21:05:53 -04:00
|
|
|
|
|
|
|
describe "#recent_push" do
|
|
|
|
subject { create(:user) }
|
2017-01-19 16:44:25 -05:00
|
|
|
let!(:project1) { create(:project, :repository) }
|
|
|
|
let!(:project2) { create(:project, :repository, forked_from_project: project1) }
|
2016-08-04 11:44:27 -04:00
|
|
|
let!(:push_data) do
|
2016-08-12 04:09:29 -04:00
|
|
|
Gitlab::DataBuilder::Push.build_sample(project2, subject)
|
2016-08-04 11:44:27 -04:00
|
|
|
end
|
2017-01-29 17:37:24 -05:00
|
|
|
let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }
|
2015-08-16 21:05:53 -04:00
|
|
|
|
|
|
|
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
|
2016-08-24 07:13:26 -04:00
|
|
|
|
|
|
|
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)
|
2017-01-29 17:37:24 -05:00
|
|
|
push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)
|
2016-08-24 07:13:26 -04:00
|
|
|
|
|
|
|
expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
|
|
|
|
end
|
2015-08-16 21:05:53 -04:00
|
|
|
end
|
2015-11-16 08:28:02 -05:00
|
|
|
|
|
|
|
describe '#authorized_groups' do
|
|
|
|
let!(:user) { create(:user) }
|
|
|
|
let!(:private_group) { create(:group) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
private_group.add_user(user, Gitlab::Access::MASTER)
|
|
|
|
end
|
|
|
|
|
2015-11-18 06:30:24 -05:00
|
|
|
subject { user.authorized_groups }
|
2015-11-16 08:28:02 -05:00
|
|
|
|
2015-11-18 06:30:24 -05:00
|
|
|
it { is_expected.to eq([private_group]) }
|
2015-11-16 08:28:02 -05:00
|
|
|
end
|
|
|
|
|
2016-10-11 08:25:17 -04:00
|
|
|
describe '#authorized_projects', truncate: true do
|
2016-07-19 15:19:04 -04:00
|
|
|
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)
|
2015-11-16 08:28:02 -05:00
|
|
|
|
2017-02-22 17:54:59 -05:00
|
|
|
expect(user.authorized_projects(Gitlab::Access::REPORTER))
|
|
|
|
.to contain_exactly(project)
|
2016-07-19 15:19:04 -04:00
|
|
|
end
|
2015-11-16 08:28:02 -05:00
|
|
|
|
2016-07-19 15:19:04 -04:00
|
|
|
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]
|
2015-11-16 08:28:02 -05:00
|
|
|
|
2017-02-22 17:54:59 -05:00
|
|
|
expect(user.authorized_projects(Gitlab::Access::REPORTER))
|
|
|
|
.to contain_exactly(project)
|
2016-07-19 15:19:04 -04:00
|
|
|
end
|
|
|
|
end
|
2016-10-11 08:25:17 -04:00
|
|
|
|
|
|
|
it "includes user's personal projects" do
|
|
|
|
user = create(:user)
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, :private, namespace: user.namespace)
|
2016-10-11 08:25:17 -04:00
|
|
|
|
|
|
|
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)
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, :private, namespace: user1.namespace)
|
2016-10-11 08:25:17 -04:00
|
|
|
|
|
|
|
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)
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, group: group)
|
2016-10-11 08:25:17 -04:00
|
|
|
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)
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, group: group)
|
2016-10-11 08:25:17 -04:00
|
|
|
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)
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, :private)
|
2016-10-11 08:25:17 -04:00
|
|
|
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)
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, :private, namespace: user1.namespace)
|
2016-10-11 08:25:17 -04:00
|
|
|
|
|
|
|
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)
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, namespace: group)
|
2016-10-11 08:25:17 -04:00
|
|
|
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
|
2015-11-16 08:28:02 -05:00
|
|
|
end
|
2016-05-10 13:03:55 -04:00
|
|
|
|
2016-08-05 09:29:20 -04:00
|
|
|
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
|
2017-01-19 16:44:25 -05:00
|
|
|
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) }
|
2016-08-05 09:29:20 -04:00
|
|
|
|
|
|
|
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
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project)
|
|
|
|
guest_project = create(:empty_project) { |p| p.add_guest(user) }
|
2016-08-05 09:29:20 -04:00
|
|
|
|
|
|
|
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
|
2017-01-19 16:44:25 -05:00
|
|
|
project = create(:empty_project, :archived)
|
2016-08-05 09:29:20 -04:00
|
|
|
|
|
|
|
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
|
2017-02-02 05:10:25 -05:00
|
|
|
project = create(:empty_project, :issues_disabled)
|
2016-08-05 09:29:20 -04:00
|
|
|
|
|
|
|
expect(user.projects_where_can_admin_issues.to_a).to be_empty
|
|
|
|
expect(user.can?(:admin_issue, project)).to eq(false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-06-01 09:00:56 -04:00
|
|
|
describe '#ci_authorized_runners' do
|
|
|
|
let(:user) { create(:user) }
|
|
|
|
let(:runner) { create(:ci_runner) }
|
|
|
|
|
2016-06-02 11:30:27 -04:00
|
|
|
before do
|
|
|
|
project.runners << runner
|
|
|
|
end
|
2016-06-01 09:00:56 -04:00
|
|
|
|
|
|
|
context 'without any projects' do
|
2017-01-19 16:44:25 -05:00
|
|
|
let(:project) { create(:empty_project) }
|
2016-06-01 09:00:56 -04:00
|
|
|
|
|
|
|
it 'does not load' do
|
2016-06-06 02:55:18 -04:00
|
|
|
expect(user.ci_authorized_runners).to be_empty
|
2016-06-01 09:00:56 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with personal projects runners' do
|
|
|
|
let(:namespace) { create(:namespace, owner: user) }
|
2017-01-19 16:44:25 -05:00
|
|
|
let(:project) { create(:empty_project, namespace: namespace) }
|
2016-06-01 09:00:56 -04:00
|
|
|
|
|
|
|
it 'loads' do
|
2016-06-06 02:55:18 -04:00
|
|
|
expect(user.ci_authorized_runners).to contain_exactly(runner)
|
2016-06-01 09:00:56 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples :member do
|
2016-06-02 01:15:45 -04:00
|
|
|
context 'when the user is a master' do
|
2016-06-02 11:30:27 -04:00
|
|
|
before do
|
|
|
|
add_user(Gitlab::Access::MASTER)
|
|
|
|
end
|
2016-06-02 01:15:45 -04:00
|
|
|
|
2016-06-06 02:55:18 -04:00
|
|
|
it 'loads' do
|
|
|
|
expect(user.ci_authorized_runners).to contain_exactly(runner)
|
|
|
|
end
|
2016-06-01 09:00:56 -04:00
|
|
|
end
|
|
|
|
|
2016-06-02 01:15:45 -04:00
|
|
|
context 'when the user is a developer' do
|
2016-06-02 11:30:27 -04:00
|
|
|
before do
|
|
|
|
add_user(Gitlab::Access::DEVELOPER)
|
|
|
|
end
|
2016-06-02 01:15:45 -04:00
|
|
|
|
2016-06-06 02:55:18 -04:00
|
|
|
it 'does not load' do
|
|
|
|
expect(user.ci_authorized_runners).to be_empty
|
|
|
|
end
|
2016-06-01 09:00:56 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with groups projects runners' do
|
|
|
|
let(:group) { create(:group) }
|
2017-01-19 16:44:25 -05:00
|
|
|
let(:project) { create(:empty_project, group: group) }
|
2016-06-01 09:00:56 -04:00
|
|
|
|
2016-06-02 01:27:44 -04:00
|
|
|
def add_user(access)
|
2016-06-01 09:00:56 -04:00
|
|
|
group.add_user(user, access)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like :member
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with other projects runners' do
|
2017-01-19 16:44:25 -05:00
|
|
|
let(:project) { create(:empty_project) }
|
2016-06-01 09:00:56 -04:00
|
|
|
|
2016-06-02 01:27:44 -04:00
|
|
|
def add_user(access)
|
2016-06-02 01:03:00 -04:00
|
|
|
project.team << [user, access]
|
2016-06-01 09:00:56 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like :member
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-10-13 11:53:47 -04:00
|
|
|
describe '#projects_with_reporter_access_limited_to' do
|
2017-01-19 16:44:25 -05:00
|
|
|
let(:project1) { create(:empty_project) }
|
|
|
|
let(:project2) { create(:empty_project) }
|
2016-10-13 11:53:47 -04:00
|
|
|
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))
|
2016-10-13 11:53:47 -04:00
|
|
|
|
|
|
|
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
|
2016-11-24 04:40:44 -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
|
|
|
describe '#all_expanded_groups' do
|
|
|
|
# foo/bar would also match foo/barbaz instead of just foo/bar and foo/bar/baz
|
2017-01-05 12:20:12 -05:00
|
|
|
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' }
|
2017-01-05 12:20:12 -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
|
|
|
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
|
2017-01-05 12:20:12 -05:00
|
|
|
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
|
2017-01-05 12:20:12 -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
|
|
|
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
|
2017-06-14 14:18:56 -04:00
|
|
|
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
|
2017-06-14 14:18:56 -04:00
|
|
|
before do
|
|
|
|
nested_group_1.add_owner(user)
|
|
|
|
end
|
2017-01-05 12:20:12 -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
|
|
|
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
|
2017-06-14 14:18:56 -04:00
|
|
|
before do
|
|
|
|
nested_group_2.add_owner(user)
|
|
|
|
end
|
2017-01-05 12:20:12 -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
|
|
|
it 'returns the groups in the hierarchy' do
|
|
|
|
is_expected.to match_array [
|
|
|
|
group,
|
|
|
|
nested_group_2, nested_group_2_1
|
|
|
|
]
|
|
|
|
end
|
2017-01-05 12:20:12 -05:00
|
|
|
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
|
2017-06-14 14:18:56 -04:00
|
|
|
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
|
2017-01-05 12:20:12 -05:00
|
|
|
end
|
|
|
|
|
2016-11-24 04:40:44 -05:00
|
|
|
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
|
2016-11-11 01:27:43 -05:00
|
|
|
|
2017-06-22 02:35:49 -04:00
|
|
|
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
|
|
|
|
|
2016-11-11 01:27:43 -05:00
|
|
|
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
|
2017-02-03 07:55:50 -05:00
|
|
|
expect(ghost.username).to eq('ghost1')
|
2016-11-11 01:27:43 -05:00
|
|
|
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
|
2017-02-03 07:55:50 -05:00
|
|
|
expect(ghost.email).to eq('ghost1@example.com')
|
2016-11-11 01:27:43 -05:00
|
|
|
end
|
|
|
|
end
|
2017-04-26 15:13:04 -04:00
|
|
|
|
|
|
|
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
|
2016-11-11 01:27:43 -05:00
|
|
|
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
|
2017-03-14 09:34:21 -04:00
|
|
|
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
|
2017-03-14 09:34:21 -04:00
|
|
|
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
|
2017-03-14 09:34:21 -04:00
|
|
|
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
|
2017-03-14 09:34:21 -04:00
|
|
|
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
|
2017-05-04 19:44:19 -04:00
|
|
|
|
|
|
|
describe 'preferred language' do
|
|
|
|
it 'is English by default' do
|
|
|
|
user = create(:user)
|
|
|
|
|
|
|
|
expect(user.preferred_language).to eq('en')
|
|
|
|
end
|
|
|
|
end
|
2017-05-15 08:04:09 -04:00
|
|
|
|
|
|
|
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
|