gitlab-ce/spec/lib/gitlab/import/source_user_mapper_spec.rb

376 lines
13 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Import::SourceUserMapper, :request_store, feature_category: :importers do
let_it_be(:namespace) { create(:group) }
let_it_be(:import_type) { 'github' }
let_it_be(:source_hostname) { 'https://github.com' }
let_it_be(:existing_import_source_user) do
create(
:import_source_user,
namespace: namespace,
import_type: import_type,
source_hostname: source_hostname,
source_user_identifier: '101')
end
let_it_be(:import_source_user_from_another_import) { create(:import_source_user) }
describe '#find_or_create_source_user' do
let_it_be(:import_user) { create(:namespace_import_user, namespace: namespace).import_user }
let(:source_name) { 'Pry Contributor' }
let(:source_username) { 'a_pry_contributor' }
let(:source_user_identifier) { '123456' }
let(:cache) { false }
subject(:find_or_create_source_user) do
described_class.new(
namespace: namespace,
import_type: import_type,
source_hostname: source_hostname
).find_or_create_source_user(
source_name: source_name,
source_username: source_username,
source_user_identifier: source_user_identifier,
cache: cache
)
end
shared_examples 'creates an import_source_user and a unique placeholder user' do
it 'creates an import_source_user with an internal placeholder user' do
expect { find_or_create_source_user }.to change { Import::SourceUser.count }.by(1)
new_import_source_user = Import::SourceUser.last
expect(new_import_source_user.placeholder_user).to be_placeholder
expect(new_import_source_user.attributes).to include({
'namespace_id' => namespace.id,
'import_type' => import_type,
'source_hostname' => source_hostname,
'source_name' => source_name,
'source_username' => source_username,
'source_user_identifier' => source_user_identifier
})
end
it 'creates a new placeholder user with a unique email and username' do
expect { find_or_create_source_user }.to change { User.where(user_type: :placeholder).count }.by(1)
new_placeholder_user = User.where(user_type: :placeholder).last
expect(new_placeholder_user.name).to eq("Placeholder #{source_name}")
expect(new_placeholder_user.username).to match(/^aprycontributor_placeholder_user_\d+$/)
expect(new_placeholder_user.email).to match(/^#{import_type}_\h+_\d+@#{Settings.gitlab.host}$/)
end
end
shared_examples 'it does not create an import_source_user or placeholder user' do
it 'does not create a import_source_user' do
expect { find_or_create_source_user }.not_to change { Import::SourceUser.count }
end
it 'does not create any internal users' do
expect { find_or_create_source_user }.not_to change { User.count }
end
end
context 'when the placeholder user limit has not been reached' do
it_behaves_like 'creates an import_source_user and a unique placeholder user'
it 'caches the created object and does not query the database multiple times' do
expect(::Import::SourceUser).to receive(:find_source_user).once.and_call_original
2.times do
expect(described_class.new(
namespace: namespace,
import_type: import_type,
source_hostname: source_hostname
).find_or_create_source_user(
source_name: source_name,
source_username: source_username,
source_user_identifier: source_user_identifier
).source_user_identifier).to eq(source_user_identifier)
end
end
context 'when another source user was created before the lease was obtained' do
let(:race_condition_source_user) do
create(:import_source_user,
namespace: namespace,
import_type: import_type,
source_hostname: source_hostname,
source_name: source_name,
source_username: source_username,
source_user_identifier: source_user_identifier
)
end
it 'returns the existing source user' do
expect_next_instance_of(described_class) do |source_user_mapper|
expect(source_user_mapper).to receive(:create_source_user).and_wrap_original do |original_method, **args|
race_condition_source_user
original_method.call(**args)
end
end
expect(find_or_create_source_user).to eq(race_condition_source_user)
end
end
context 'when retried and another source user is not created while waiting' do
before do
allow_next_instance_of(described_class) do |source_user_mapper|
allow(source_user_mapper).to receive(:in_lock).and_yield(true)
end
end
it_behaves_like 'creates an import_source_user and a unique placeholder user'
end
context 'when retried and another source user was made while waiting' do
before do
allow_next_instance_of(described_class) do |source_user_mapper|
allow(source_user_mapper).to receive(:in_lock).and_yield(true)
end
allow(Import::SourceUser).to receive(:find_source_user).and_return(nil, existing_import_source_user)
end
it 'returns the existing source user' do
expect(find_or_create_source_user).to eq(existing_import_source_user)
end
it_behaves_like 'it does not create an import_source_user or placeholder user'
end
context 'and an import source user exists for current import source' do
let(:source_user_identifier) { existing_import_source_user.source_user_identifier }
it 'returns the existing source user' do
expect(find_or_create_source_user).to eq(existing_import_source_user)
end
it_behaves_like 'it does not create an import_source_user or placeholder user'
end
context 'when source host name has a path' do
let(:source_hostname) { 'https://github.com/path' }
it 'normalizes the source_hostname' do
expect(find_or_create_source_user.source_hostname).to eq('https://github.com')
end
end
context 'when source host name has a port' do
let(:source_hostname) { 'https://github.com:8443/path' }
it 'normalizes the base URI and keeps the port in the source_hostname' do
expect(find_or_create_source_user.source_hostname).to eq('https://github.com:8443')
end
end
context 'when source host name has a subdomain' do
let(:source_hostname) { 'https://subdomain.github.com/path' }
it 'normalizes the base URI and keeps the subdomain in the source_hostname' do
expect(find_or_create_source_user.source_hostname).to eq('https://subdomain.github.com')
end
end
end
context 'when the placeholder user limit has been reached' do
before do
allow_next_instance_of(Import::PlaceholderUserLimit) do |limit|
allow(limit).to receive(:exceeded?).and_return(true)
end
end
it 'does not create any placeholder users and assigns the import user' do
expect { find_or_create_source_user }
.to change { Import::SourceUser.count }.by(1)
.and not_change { User.count }
new_import_source_user = Import::SourceUser.last
expect(new_import_source_user.placeholder_user).to eq(import_user)
end
end
context 'when namespace is a personal namespace' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:import_user) { create(:namespace_import_user, namespace: namespace).import_user }
it 'does not create any placeholder users and assigns the import user' do
expect { find_or_create_source_user }
.to change { Import::SourceUser.count }.by(1)
new_import_source_user = Import::SourceUser.last
expect(new_import_source_user.placeholder_user).to eq(import_user)
end
end
context 'when ActiveRecord::RecordNotUnique exception is raised during the source user creation' do
before do
allow_next_instance_of(::Import::SourceUser) do |source_user|
allow(source_user).to receive(:save!).and_raise(ActiveRecord::RecordNotUnique)
end
end
it 'raises DuplicatedUserError' do
expect { find_or_create_source_user }.to raise_error(described_class::DuplicatedUserError)
end
end
context 'when ActiveRecord::RecordInvalid exception because the placeholder user email or username is taken' do
it 'rescue the exception and raises DuplicatedUserError' do
create(:user, email: 'user@example.com')
user = build(:user, email: 'user@example.com').tap(&:valid?)
allow(User).to receive(:new).and_return(user)
expect { find_or_create_source_user }.to raise_error(described_class::DuplicatedUserError)
end
end
context 'when ActiveRecord::RecordInvalid exception raises for another reason' do
it 'bubbles up the ActiveRecord::RecordInvalid exception' do
user = build(:user, email: nil)
allow(User).to receive(:new).and_return(user)
expect { find_or_create_source_user }.to raise_error(ActiveRecord::RecordInvalid)
end
end
context 'when cache is true' do
let(:cache) { true }
it 'caches the created source user' do
source_user = find_or_create_source_user
expect(Gitlab::SafeRequestStore[:source_user_cache][source_user.source_user_identifier]).to eq(source_user)
end
end
context 'when cache is false' do
let(:cache) { false }
it 'does not cache the created source user' do
source_user = find_or_create_source_user
expect(Gitlab::SafeRequestStore[:source_user_cache][source_user.source_user_identifier]).to eq(nil)
end
end
end
describe '#find_source_user' do
let(:source_user_identifier) { existing_import_source_user.source_user_identifier }
subject(:find_source_user) do
described_class.new(
namespace: namespace,
import_type: import_type,
source_hostname: source_hostname
).find_source_user(source_user_identifier)
end
it 'returns the existing source user' do
expect(find_source_user).to eq(existing_import_source_user)
end
context 'when source_hostname has a path, and the source user record does not' do
let(:source_hostname) { 'https://github.com/path' }
it 'returns the existing source user' do
expect(find_source_user).to eq(existing_import_source_user)
expect(existing_import_source_user.source_hostname).to eq('https://github.com')
end
end
context 'when source_hostname has a port, and the source user record does not' do
let(:source_hostname) { 'https://github.com:8443' }
it 'does not return the existing source user' do
expect(find_source_user).to be_nil
end
end
context 'when source_hostname has a subdomain, and the source user record does not' do
let(:source_hostname) { 'https://subdomain.github.com' }
it 'does not return the existing source user' do
expect(find_source_user).to be_nil
end
end
context 'when source_hostname scheme does not match' do
let(:source_hostname) { 'http://github.com' }
it 'does not return the existing source user' do
expect(find_source_user).to be_nil
end
end
context 'when namespace does not match' do
let(:namespace) { create(:group) }
it 'does not return the existing source user' do
expect(find_source_user).to be_nil
end
end
context 'when import_type does not match' do
let(:import_type) { 'gitea' }
it 'does not return the existing source user' do
expect(find_source_user).to be_nil
end
end
context 'when source user does not exist' do
let(:source_user_identifier) { '999999' }
it { is_expected.to be_nil }
it 'does not cache the result and queries the database multiple times' do
expect(::Import::SourceUser).to receive(:find_source_user).twice.and_call_original
2.times do
described_class.new(
namespace: namespace,
import_type: import_type,
source_hostname: source_hostname
).find_source_user(source_user_identifier)
end
end
end
context 'when called multiple times' do
it 'returns the same result' do
expect(find_source_user).to eq(
described_class.new(
namespace: namespace,
import_type: import_type,
source_hostname: source_hostname
).find_source_user(source_user_identifier)
)
end
it 'caches the result and does not query the database multiple times' do
expect(::Import::SourceUser).to receive(:find_source_user).once.and_call_original
2.times do
described_class.new(
namespace: namespace,
import_type: import_type,
source_hostname: source_hostname
).find_source_user(source_user_identifier)
end
end
end
end
end