gitlab-ce/spec/models/import/source_user_spec.rb

592 lines
21 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do
describe 'associations' do
it { is_expected.to belong_to(:placeholder_user).class_name('User') }
it { is_expected.to belong_to(:reassign_to_user).class_name('User') }
it { is_expected.to belong_to(:namespace) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:namespace_id) }
it { is_expected.to validate_presence_of(:import_type) }
it { is_expected.to validate_presence_of(:placeholder_user_id) }
it { is_expected.to validate_presence_of(:source_hostname) }
it { is_expected.to validate_presence_of(:source_user_identifier) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to validate_absence_of(:reassignment_token) }
it { is_expected.not_to validate_presence_of(:reassign_to_user_id) }
it 'validates source_hostname has port and scheme' do
create(:import_source_user)
is_expected.to allow_value("http://example.com:8080", "http://example.com").for(:source_hostname)
is_expected.not_to allow_values("http://", "example.com", "http://example.com/dir").for(:source_hostname)
end
it 'validates uniqueness of reassign_to_user_id' do
create(:import_source_user, :reassignment_in_progress)
is_expected.to validate_uniqueness_of(:reassign_to_user_id)
.scoped_to(:namespace_id, :source_hostname, :import_type)
.with_message('already assigned to another placeholder')
end
it 'validates uniqueness of source_user_identifier' do
create(:import_source_user)
is_expected.to validate_uniqueness_of(:source_user_identifier)
.scoped_to(:namespace_id, :source_hostname, :import_type)
end
context 'when completed' do
subject { build(:import_source_user, :completed) }
it { is_expected.not_to validate_presence_of(:placeholder_user_id) }
it { is_expected.to validate_presence_of(:reassign_to_user_id) }
end
context 'when awaiting_approval' do
subject { build(:import_source_user, :awaiting_approval) }
it { is_expected.to validate_presence_of(:reassign_to_user_id) }
it { is_expected.to validate_length_of(:reassignment_token).is_equal_to(32) }
it { is_expected.to validate_presence_of(:reassignment_token).with_message(/is the wrong length/) }
end
context 'when reassignment_in_progress' do
subject { build(:import_source_user, :reassignment_in_progress) }
it { is_expected.to validate_presence_of(:reassign_to_user_id) }
end
context 'when rejected' do
subject { build(:import_source_user, :rejected) }
it { is_expected.not_to validate_absence_of(:reassign_to_user_id) }
end
context 'when pending_reassignment' do
subject { build(:import_source_user, :pending_reassignment) }
it { is_expected.to validate_absence_of(:reassign_to_user_id) }
end
context 'when keep_as_placeholder' do
subject { build(:import_source_user, :keep_as_placeholder) }
it { is_expected.to validate_absence_of(:reassign_to_user_id) }
end
context 'for validate_source_hostname' do
let(:namespace) { create(:namespace) }
let(:placeholder_user) { create(:import_source_user, namespace: namespace).placeholder_user }
let(:import_source_user) do
build(:import_source_user, placeholder_user: placeholder_user, namespace: namespace,
source_hostname: 'example.com')
end
context 'when source_hostname is changed' do
it 'validates the format of source_hostname' do
expect(import_source_user).not_to be_valid
expect(import_source_user.errors[:source_hostname]).to include(
"must contain scheme and host, and not path"
)
end
it 'passes validation for correct hostname' do
import_source_user.source_hostname = 'http://example.com:8080'
expect(import_source_user).to be_valid
end
end
context 'when source_hostname is not changed' do
it 'skips validation' do
# First save with invalid hostname but skip validation
import_source_user.save!(validate: false)
# Now update a different attribute
import_source_user.source_name = 'New Name'
# Validation should be skipped since source_hostname didn't change
expect(import_source_user).to be_valid
end
end
context 'when updating an existing record with the same source_hostname' do
it 'skips validation' do
# First save with invalid hostname but skip validation
import_source_user.save!(validate: false)
# Reload and try to save again without changing source_hostname
reloaded_user = described_class.find(import_source_user.id)
# Should be valid because source_hostname_changed? is false
expect(reloaded_user).to be_valid
end
end
context 'when updating an existing record with a new invalid source_hostname' do
it 'fails validation' do
# First save with valid hostname
import_source_user.source_hostname = 'http://example.com'
import_source_user.save!
# Now update to an invalid hostname
import_source_user.source_hostname = 'example.com'
# Should fail validation because source_hostname_changed? is true
expect(import_source_user).not_to be_valid
expect(import_source_user.errors[:source_hostname]).to include(
"must contain scheme and host, and not path"
)
end
end
end
end
describe 'scopes' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:source_user_1) { create(:import_source_user, namespace: namespace) }
let_it_be(:source_user_2) { create(:import_source_user, namespace: namespace) }
let_it_be(:source_user_3) { create(:import_source_user) }
describe '.for_namespace' do
it 'only returns source users for the given namespace_id' do
expect(described_class.for_namespace(namespace.id).to_a).to match_array(
[source_user_1, source_user_2]
)
end
end
describe '.for_placeholder_user' do
let(:placeholder_user) { source_user_2.placeholder_user }
it 'only returns source users for the given placeholder user' do
expect(described_class.for_placeholder_user(placeholder_user).to_a).to match_array(
[source_user_2]
)
end
end
describe '.awaiting_reassignment' do
it 'only returns source users that await reassignment' do
namespace = create(:namespace)
pending_reassignment_user = create(:import_source_user, :pending_reassignment, namespace: namespace)
awaiting_approval_user = create(:import_source_user, :awaiting_approval, namespace: namespace)
expect(namespace.import_source_users.awaiting_reassignment)
.to match_array([pending_reassignment_user, awaiting_approval_user])
end
end
describe '.reassigned' do
it 'only returns source users with status completed' do
namespace = create(:namespace)
completed_assignment_user = create(:import_source_user, :completed, namespace: namespace)
placeholder_user = create(:import_source_user, :keep_as_placeholder, namespace: namespace)
expect(described_class.for_namespace(namespace.id).to_a)
.to match_array([completed_assignment_user, placeholder_user])
end
end
end
describe 'state machine' do
it 'begins in pending state' do
expect(described_class.new.pending_reassignment?).to eq(true)
end
context 'when switching to awaiting_approval' do
subject(:source_user) { create(:import_source_user, :pending_reassignment) }
it 'assigns a reassignment_token' do
expect { source_user.reassign }.to change { source_user.reassignment_token }.from(nil)
end
end
context 'when switching from awaiting_approval' do
subject(:source_user) { create(:import_source_user, :awaiting_approval) }
it 'removes the reassignment_token' do
expect { source_user.cancel_reassignment }.to change { source_user.reassignment_token }.to(nil)
end
end
context 'when switching to reassignment_in_progress without reassigned to user approval' do
let_it_be(:reassign_to_user) { create(:user) }
let_it_be(:reassigned_by_user) { create(:user, :admin) }
subject(:source_user) { create(:import_source_user, :pending_reassignment) }
before do
source_user.reassign_to_user = reassign_to_user
source_user.reassigned_by_user = reassigned_by_user
end
context 'and admin bypass placeholder user confirmation is allowed' do
before do
expect_next_instance_of(Import::UserMapping::AdminBypassAuthorizer, reassigned_by_user) do |authorizer|
allow(authorizer).to receive(:allowed?).and_return(true)
end
end
it 'allows the transition' do
expect(source_user.reassign_without_confirmation).to be(true)
end
end
context 'and admins bypass placeholder user confirmation is not allowed' do
before do
expect_next_instance_of(Import::UserMapping::AdminBypassAuthorizer, reassigned_by_user) do |authorizer|
allow(authorizer).to receive(:allowed?).and_return(false)
end
end
it 'does not allow the transition' do
expect(source_user.reassign_without_confirmation).to be(false)
end
end
end
end
describe '.find_source_user' do
let_it_be(:namespace_1) { create(:namespace) }
let_it_be(:namespace_2) { create(:namespace) }
let_it_be(:source_user_1) { create(:import_source_user, source_user_identifier: '1', namespace: namespace_1) }
let_it_be(:source_user_2) { create(:import_source_user, source_user_identifier: '2', namespace: namespace_1) }
let_it_be(:source_user_3) { create(:import_source_user, source_user_identifier: '1', namespace: namespace_2) }
let_it_be(:source_user_4) do
create(:import_source_user,
source_user_identifier: '1',
namespace: namespace_1,
import_type: 'bitbucket',
source_hostname: 'https://bitbucket.org'
)
end
let_it_be(:source_user_5) do
create(:import_source_user,
source_user_identifier: '1',
namespace: namespace_1,
source_hostname: 'https://bitbucket-server-domain.com',
import_type: 'bitbucket_server'
)
end
it 'returns the first source_user that matches the source_user_identifier for the import source attributes' do
expect(described_class.find_source_user(
source_user_identifier: '1',
namespace: namespace_1,
source_hostname: 'https://github.com',
import_type: 'github'
)).to eq(source_user_1)
end
it 'does not throw an error when any attributes are nil' do
expect do
described_class.find_source_user(
source_user_identifier: nil,
namespace: nil,
source_hostname: nil,
import_type: nil
)
end.not_to raise_error
end
it 'returns nil if no namespace is provided' do
expect(described_class.find_source_user(
source_user_identifier: '1',
namespace: nil,
source_hostname: 'https://github.com',
import_type: 'github'
)).to be_nil
end
end
describe '.search' do
let!(:source_user) do
create(:import_source_user, source_name: 'Source Name', source_username: 'Username')
end
it 'searches by source_name or source_username' do
expect(described_class.search('name')).to eq([source_user])
expect(described_class.search('username')).to eq([source_user])
expect(described_class.search('source')).to eq([source_user])
expect(described_class.search('inexistent')).to eq([])
end
end
describe '.sort_by_attribute' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:source_user_1) do
create(:import_source_user, namespace: namespace, status: 4, source_name: 'd')
end
let_it_be(:source_user_2) do
create(:import_source_user, namespace: namespace, status: 3, source_name: 'c')
end
let_it_be(:source_user_3) do
create(
:import_source_user,
:with_reassign_to_user,
namespace: namespace,
status: 1,
source_name: 'a',
reassignment_token: SecureRandom.hex
)
end
let_it_be(:source_user_4) do
create(:import_source_user, :with_reassign_to_user, namespace: namespace, status: 2, source_name: 'b')
end
let(:sort_by_attribute) { described_class.sort_by_attribute(method).pluck(attribute) }
context 'with method status_asc' do
let(:method) { 'status_asc' }
let(:attribute) { :status }
it 'order by status_desc ascending' do
expect(sort_by_attribute).to eq([1, 2, 3, 4])
end
end
context 'with method status_desc' do
let(:method) { 'status_desc' }
let(:attribute) { :status }
it 'order by status_desc descending' do
expect(sort_by_attribute).to eq([4, 3, 2, 1])
end
end
context 'with method source_name_asc' do
let(:method) { 'source_name_asc' }
let(:attribute) { :source_name }
it 'order by source_name_asc ascending' do
expect(sort_by_attribute).to eq(%w[a b c d])
end
end
context 'with method source_name_desc' do
let(:method) { 'source_name_desc' }
let(:attribute) { :source_name }
it 'order by source_name_desc descending' do
expect(sort_by_attribute).to eq(%w[d c b a])
end
end
context 'with method id_asc' do
let(:method) { 'id_asc' }
let(:attribute) { :id }
it 'order by id_asc ascending' do
expect(sort_by_attribute).to eq([
source_user_1.id,
source_user_2.id,
source_user_3.id,
source_user_4.id
])
end
end
context 'with method id_desc' do
let(:method) { 'id_desc' }
let(:attribute) { :id }
it 'order by id_desc descending' do
expect(sort_by_attribute).to eq([
source_user_4.id,
source_user_3.id,
source_user_2.id,
source_user_1.id
])
end
end
context 'with an unexpected method' do
let(:method) { 'unknown_sort' }
let(:attribute) { :source_name }
it 'order by source_name_asc ascending' do
expect(sort_by_attribute).to eq(%w[a b c d])
end
end
end
describe '.namespace_placeholder_user_count' do
let_it_be_with_refind(:namespace) { create(:namespace) }
subject(:namespace_placeholder_user_count) do
described_class.namespace_placeholder_user_count(namespace, limit: 100)
end
before_all do
placeholder_user = create(:import_source_user, namespace: namespace).placeholder_user
create_list(:import_source_user, 2, placeholder_user: placeholder_user, namespace: namespace)
create(:import_source_user, :completed, placeholder_user: nil, namespace: namespace)
create(:import_source_user)
end
it 'returns the count of records with unique placeholder users for the namespace' do
expect(namespace_placeholder_user_count).to eq(1)
end
it 'does not count the import user type' do
import_user = create(:namespace_import_user, namespace: namespace).import_user
create(:import_source_user, placeholder_user: import_user, namespace: namespace)
expect(namespace_placeholder_user_count).to eq(1)
end
it 'can limit the count to optimize the query' do
create(:import_source_user, namespace: namespace)
expect(described_class.namespace_placeholder_user_count(namespace, limit: 1)).to eq(1)
expect(described_class.namespace_placeholder_user_count(namespace, limit: 2)).to eq(2)
expect(described_class.namespace_placeholder_user_count(namespace, limit: 3)).to eq(2)
end
end
describe '.source_users_with_missing_information' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:import_type) { 'github' }
let_it_be(:source_hostname) { 'https://github.com' }
let_it_be(:source_user_1) do
create(:import_source_user, namespace: namespace, source_username: nil, source_name: nil,
import_type: import_type, source_hostname: source_hostname)
end
let_it_be(:source_user_2) do
create(:import_source_user, namespace: namespace, source_username: 'a', source_name: nil,
import_type: import_type, source_hostname: source_hostname)
end
let_it_be(:source_user_3) do
create(:import_source_user, namespace: namespace, source_username: nil, source_name: 'b',
import_type: import_type, source_hostname: source_hostname)
end
before_all do
create(:import_source_user, namespace: namespace, source_username: 'a', source_name: 'b',
import_type: import_type, source_hostname: source_hostname)
create(:import_source_user, source_username: nil, source_name: nil, import_type: import_type,
source_hostname: source_hostname)
create(:import_source_user, namespace: namespace, source_username: nil, source_name: nil,
import_type: 'gitlab_importer', source_hostname: source_hostname)
create(:import_source_user, namespace: namespace, source_username: nil, source_name: nil,
import_type: import_type, source_hostname: 'http://source_hostname')
end
it 'returns the count of records with unique placeholder users for the namespace' do
results = described_class.source_users_with_missing_information(namespace: namespace, import_type: import_type,
source_hostname: source_hostname)
expect(results).to match_array([source_user_1, source_user_2, source_user_3])
end
end
describe '#mapped_user' do
let_it_be(:source_user) { build(:import_source_user, :with_reassign_to_user) }
subject(:mapped_user) { source_user.mapped_user }
before do
allow(source_user).to receive(:accepted_status?).and_return(accepted)
end
context 'when accepted' do
let(:accepted) { true }
it { is_expected.to eq(source_user.reassign_to_user) }
end
context 'when not accepted' do
let(:accepted) { false }
it { is_expected.to eq(source_user.placeholder_user) }
end
end
describe '#mapped_user_id' do
let_it_be(:source_user) { build(:import_source_user, :with_reassign_to_user) }
subject(:mapped_user_id) { source_user.mapped_user_id }
before do
allow(source_user).to receive(:accepted_status?).and_return(accepted)
end
context 'when accepted' do
let(:accepted) { true }
it { is_expected.to eq(source_user.reassign_to_user_id) }
end
context 'when not accepted' do
let(:accepted) { false }
it { is_expected.to eq(source_user.placeholder_user_id) }
end
end
describe '#reassignable_status?' do
reassignable_statuses = [:pending_reassignment, :rejected]
all_states = described_class.state_machines[:status].states
all_states.reject { |state| reassignable_statuses.include?(state.name) }.each do |state|
it "returns false for #{state.name}" do
expect(described_class.new(status: state.value)).not_to be_reassignable_status
end
end
all_states.select { |state| reassignable_statuses.include?(state.name) }.each do |state|
it "returns true for #{state.name}" do
expect(described_class.new(status: state.value)).to be_reassignable_status
end
end
end
describe '#cancelable_status?' do
cancelable_statuses = [:awaiting_approval, :rejected]
all_states = described_class.state_machines[:status].states
all_states.reject { |state| cancelable_statuses.include?(state.name) }.each do |state|
it "returns false for #{state.name}" do
expect(described_class.new(status: state.value)).not_to be_cancelable_status
end
end
all_states.select { |state| cancelable_statuses.include?(state.name) }.each do |state|
it "returns true for #{state.name}" do
expect(described_class.new(status: state.value)).to be_cancelable_status
end
end
end
describe '#accepted_status?' do
accepted_statuses = [:reassignment_in_progress, :completed, :failed]
all_states = described_class.state_machines[:status].states
all_states.reject { |state| accepted_statuses.include?(state.name) }.each do |state|
it "returns false for #{state.name}" do
expect(described_class.new(status: state.value)).not_to be_accepted_status
end
end
all_states.select { |state| accepted_statuses.include?(state.name) }.each do |state|
it "returns true for #{state.name}" do
expect(described_class.new(status: state.value)).to be_accepted_status
end
end
end
end