Files
gitlab-foss/spec/models/bulk_import_spec.rb
2025-04-08 12:13:19 +00:00

359 lines
11 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImport, type: :model, feature_category: :importers do
using RSpec::Parameterized::TableSyntax
let_it_be(:created_bulk_import) { create(:bulk_import, :created, updated_at: 2.hours.ago) }
let_it_be(:started_bulk_import) { create(:bulk_import, :started, updated_at: 3.hours.ago) }
let_it_be(:finished_bulk_import) { create(:bulk_import, :finished, updated_at: 1.hour.ago) }
let_it_be(:failed_bulk_import) { create(:bulk_import, :failed) }
let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, updated_at: 3.days.ago) }
let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, updated_at: 2.days.ago) }
describe 'associations' do
it { is_expected.to belong_to(:user).required }
it { is_expected.to belong_to(:organization) }
it { is_expected.to have_one(:configuration) }
it { is_expected.to have_many(:entities) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:source_type) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to define_enum_for(:source_type).with_values(%i[gitlab]) }
end
describe 'scopes' do
describe '.stale' do
subject { described_class.stale }
it { is_expected.to contain_exactly(stale_created_bulk_import, stale_started_bulk_import) }
end
describe '.order_by_updated_at_and_id' do
subject { described_class.order_by_updated_at_and_id(:desc) }
it 'sorts by given direction' do
is_expected.to eq([
failed_bulk_import,
finished_bulk_import,
created_bulk_import,
started_bulk_import,
stale_started_bulk_import,
stale_created_bulk_import
])
end
end
describe '.with_configuration' do
it 'includes configuration association' do
imports = described_class.with_configuration
expect(imports.first.association_cached?(:configuration)).to be(true)
end
end
end
describe '.all_human_statuses' do
it 'returns all human readable entity statuses' do
expect(described_class.all_human_statuses)
.to contain_exactly('created', 'started', 'finished', 'failed', 'timeout', 'canceled')
end
end
describe '.min_gl_version_for_project' do
it { expect(described_class.min_gl_version_for_project_migration).to be_a(Gitlab::VersionInfo) }
it { expect(described_class.min_gl_version_for_project_migration.to_s).to eq('14.4.0') }
end
describe '#completed?' do
it { expect(described_class.new(status: -2)).to be_completed }
it { expect(described_class.new(status: -1)).to be_completed }
it { expect(described_class.new(status: 0)).not_to be_completed }
it { expect(described_class.new(status: 1)).not_to be_completed }
it { expect(described_class.new(status: 2)).to be_completed }
it { expect(described_class.new(status: 3)).to be_completed }
end
describe '#source_version_info' do
it 'returns source_version as Gitlab::VersionInfo' do
bulk_import = build(:bulk_import, source_version: '9.13.2')
expect(bulk_import.source_version_info).to be_a(Gitlab::VersionInfo)
expect(bulk_import.source_version_info.to_s).to eq(bulk_import.source_version)
end
end
describe '#update_has_failures' do
let(:import) { create(:bulk_import, :started) }
let(:entity) { create(:bulk_import_entity, bulk_import: import) }
context 'when entity has failures' do
it 'sets has_failures flag to true' do
expect(import.has_failures).to eq(false)
entity.update!(has_failures: true)
import.fail_op!
expect(import.has_failures).to eq(true)
end
end
context 'when entity does not have failures' do
it 'sets has_failures flag to false' do
expect(import.has_failures).to eq(false)
entity.update!(has_failures: false)
import.fail_op!
expect(import.has_failures).to eq(false)
end
end
end
describe '#supports_batched_export?' do
context 'when source version is greater than min supported version for batched migrations' do
it 'returns true' do
bulk_import = build(:bulk_import, source_version: '16.2.0')
expect(bulk_import.supports_batched_export?).to eq(true)
end
end
context 'when source version is less than min supported version for batched migrations' do
it 'returns false' do
bulk_import = build(:bulk_import, source_version: '15.5.0')
expect(bulk_import.supports_batched_export?).to eq(false)
end
end
end
describe 'import canceling' do
let(:import) { create(:bulk_import, :started) }
it 'marks import as canceled' do
expect(import.canceled?).to eq(false)
import.cancel!
expect(import.canceled?).to eq(true)
end
context 'when import has entities' do
it 'marks entities as canceled' do
entity = create(:bulk_import_entity, bulk_import: import)
expect(entity.canceled?).to eq(false)
import.cancel!
expect(entity.reload.canceled?).to eq(true)
end
end
end
describe 'completion notification trigger' do
RSpec::Matchers.define :send_completion_notification do
def supports_block_expectations?
true
end
match(notify_expectation_failures: true) do |proc|
expect(Notify).to receive(:bulk_import_complete).with(import.user.id, import.id).and_call_original
proc.call
true
end
match_when_negated(notify_expectation_failures: true) do |proc|
expect(Notify).not_to receive(:bulk_import_complete)
proc.call
true
end
end
subject(:import) { create(:bulk_import, :started) }
let(:non_triggering_events) do
import.status_paths.events - %i[finish cleanup_stale fail_op]
end
it { expect { import.finish! }.to send_completion_notification }
it { expect { import.fail_op! }.to send_completion_notification }
it { expect { import.cleanup_stale! }.to send_completion_notification }
it "does not email after non-completing events" do
non_triggering_events.each do |event|
expect { import.send(:"#{event}!") }.not_to send_completion_notification
end
end
end
describe '#destination_group_roots' do
let_it_be(:import) { create(:bulk_import, :started) }
let_it_be(:project_namespace) { create(:group) }
let_it_be(:project) { create(:project, namespace: project_namespace) }
let_it_be(:root_project_entity) do
create(:bulk_import_entity, :project_entity, project: project, bulk_import: import)
end
let_it_be(:top_level_group) { create(:group) }
let_it_be(:root_group_entity) do
create(:bulk_import_entity, :group_entity, group: top_level_group, bulk_import: import)
end
let_it_be(:child_group_entity) do
create(:bulk_import_entity, parent: root_group_entity, bulk_import: import)
end
it 'returns the topmost group nodes of the import entity tree' do
expect(import.destination_group_roots).to match_array([project_namespace, top_level_group])
end
end
describe '#source_url' do
it 'returns migration source url via configuration' do
import = create(:bulk_import, :with_configuration)
expect(import.source_url).to eq(import.configuration.url)
end
context 'when configuration is missing' do
it 'returns nil' do
import = create(:bulk_import)
expect(import.source_url).to be_nil
end
end
end
describe '#source_equals_destination?' do
subject(:bulk_import) do
build_stubbed(:bulk_import,
configuration: build_stubbed(:bulk_import_configuration, url: source_url)
)
end
before do
allow(Settings.gitlab).to receive(:host).and_return('gitlab.example')
end
where(:source_url, :value) do
'https://gitlab.example' | true
'https://gitlab.example:443' | true
'https://gitlab.example/' | true
'https://gitlab.example/dir' | true
'http://gitlab.example' | true
'https://gitlab.example2' | false
'https://subdomain.example' | false
'https://subdomain.gitlab.example' | false
'http://192.168.1.1' | false
end
with_them do
it { expect(bulk_import.source_equals_destination?).to eq(value) }
end
end
describe '#namespaces_with_unassigned_placeholders' do
let_it_be(:group) { create(:group) }
let_it_be(:entity) do
create(:bulk_import_entity, :group_entity, bulk_import: finished_bulk_import, group: group)
end
before do
create_list(:import_source_user, 5, :completed, namespace: group)
end
context 'when all placeholders have been assigned' do
it { expect(finished_bulk_import.namespaces_with_unassigned_placeholders).to be_empty }
end
context 'when some placeholders have not been assigned' do
before do
create(:import_source_user, :pending_reassignment, namespace: group)
end
it { expect(finished_bulk_import.namespaces_with_unassigned_placeholders).to include(group) }
end
end
describe '#schedule_configuration_purge' do
subject(:schedule_purge) { bulk_import.schedule_configuration_purge }
context 'when configuration exists' do
let_it_be(:bulk_import) { create(:bulk_import, :with_configuration) }
let(:configuration_id) { bulk_import.configuration.id }
let(:delay) { 24.hours }
before do
allow(bulk_import).to receive(:run_after_commit).and_yield
stub_const('BulkImport::PURGE_CONFIGURATION_DELAY', delay)
end
it 'schedules purge worker with default delay' do
expect(Import::BulkImports::ConfigurationPurgeWorker).to receive(:perform_in).with(delay, configuration_id)
schedule_purge
end
end
context 'when configuration does not exist' do
let_it_be(:bulk_import) { create(:bulk_import) }
it 'does not schedule any job' do
expect(Import::BulkImports::ConfigurationPurgeWorker).not_to receive(:perform_in)
schedule_purge
end
end
end
describe 'state-machine transitions for configuration purging' do
RSpec::Matchers.define :schedule_configuration_purge do
def supports_block_expectations?
true
end
match(notify_expectation_failures: true) do |proc|
expect_next_instance_of(described_class) do |instance|
expect(instance).to receive(:schedule_configuration_purge).and_call_original
end
proc.call
true
end
match_when_negated(notify_expectation_failures: true) do |proc|
expect_next_instance_of(described_class) do |instance|
expect(instance).not_to receive(:schedule_configuration_purge)
end
proc.call
true
end
end
context "when bulk import transitions to a completed state" do
subject(:import) { create(:bulk_import, :started, :with_configuration) }
it { expect { import.finish! }.to schedule_configuration_purge }
it { expect { import.fail_op! }.to schedule_configuration_purge }
it { expect { import.cleanup_stale! }.to schedule_configuration_purge }
it { expect { import.cancel }.to schedule_configuration_purge }
end
context "when bulk import transitions to a non-completed state" do
subject(:import) { create(:bulk_import, :created, :with_configuration) }
it { expect { import.start }.not_to schedule_configuration_purge }
end
end
end